+ client: handle blocked services

This commit is contained in:
Ildar Kamalov 2019-07-18 14:52:47 +03:00 committed by Simon Zolin
parent 3c684d1f85
commit 92cebd5b31
26 changed files with 803 additions and 126 deletions

6
client/package-lock.json generated vendored
View File

@ -945,9 +945,9 @@
} }
}, },
"axios": { "axios": {
"version": "0.18.1", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": { "requires": {
"follow-redirects": "1.5.10", "follow-redirects": "1.5.10",
"is-buffer": "^2.0.2" "is-buffer": "^2.0.2"

2
client/package.json vendored
View File

@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@nivo/line": "^0.49.1", "@nivo/line": "^0.49.1",
"axios": "^0.18.1", "axios": "^0.19.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"date-fns": "^1.29.0", "date-fns": "^1.29.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",

View File

@ -344,5 +344,14 @@
"form_answer": "Enter IP address or domain name", "form_answer": "Enter IP address or domain name",
"form_error_domain_format": "Invalid domain format", "form_error_domain_format": "Invalid domain format",
"form_error_answer_format": "Invalid answer 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"
} }

View 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());
}
};

View File

@ -509,4 +509,22 @@ export default class Api {
}; };
return this.makeRequest(path, method, parameters); 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);
}
} }

View File

@ -19,6 +19,11 @@
overflow: hidden; overflow: hidden;
} }
.logs__row--icons {
max-width: 180px;
flex-flow: row wrap;
}
.logs__row .list-unstyled { .logs__row .list-unstyled {
margin-bottom: 0; margin-bottom: 0;
overflow: hidden; overflow: hidden;
@ -26,6 +31,7 @@
.logs__text, .logs__text,
.logs__row .list-unstyled li { .logs__row .list-unstyled li {
padding: 0 1px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;

View File

@ -8,6 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link'; import { HashLink as Link } from 'react-router-hash-link';
import { formatTime, getClientName } from '../../helpers/helpers'; import { formatTime, getClientName } from '../../helpers/helpers';
import { SERVICES } from '../../helpers/constants';
import { getTrackerData } from '../../helpers/trackers/trackers'; import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card'; import Card from '../ui/Card';
@ -39,12 +40,8 @@ class Logs extends Component {
} }
} }
renderTooltip(isFiltered, rule, filter) { renderTooltip = (isFiltered, rule, filter, service) =>
if (rule) { isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
return (isFiltered && <PopoverFiltered rule={rule} filter={filter}/>);
}
return '';
}
toggleBlocking = (type, domain) => { toggleBlocking = (type, domain) => {
const { userRules } = this.props.filtering; 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) { if (isFiltered) {
return ( return (
<div className="logs__row"> <div className="logs__row">

View File

@ -17,10 +17,19 @@ class ClientsTable extends Component {
}; };
handleSubmit = (values) => { 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) { if (this.props.modalType === MODAL_TYPE.EDIT) {
this.handleFormUpdate(values, this.props.modalClientName); this.handleFormUpdate(config, this.props.modalClientName);
} else { } else {
this.handleFormAdd(values); this.handleFormAdd(config);
} }
}; };
@ -41,6 +50,7 @@ class ClientsTable extends Component {
return { return {
identifier, identifier,
use_global_settings: true, use_global_settings: true,
use_global_blocked_services: true,
...client, ...client,
}; };
} }
@ -48,6 +58,7 @@ class ClientsTable extends Component {
return { return {
identifier: CLIENT_ID.IP, identifier: CLIENT_ID.IP,
use_global_settings: true, 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'), Header: this.props.t('table_statistics'),
accessor: 'statistics', accessor: 'statistics',

View File

@ -5,18 +5,46 @@ import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form'; import Tabs from '../../ui/Tabs';
import { CLIENT_ID } from '../../../helpers/constants'; 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) => { let Form = (props) => {
const { const {
t, t,
handleSubmit, handleSubmit,
reset, reset,
change,
pristine, pristine,
submitting, submitting,
clientIdentifier, clientIdentifier,
useGlobalSettings, useGlobalSettings,
useGlobalServices,
toggleClientModal, toggleClientModal,
processingAdding, processingAdding,
processingUpdating, processingUpdating,
@ -26,31 +54,31 @@ let Form = (props) => {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="modal-body"> <div className="modal-body">
<div className="form__group"> <div className="form__group">
<div className="form-inline mb-3"> <div className="form__inline mb-2">
<strong className="mr-3"> <strong className="mr-3">
<Trans>client_identifier</Trans> <Trans>client_identifier</Trans>
</strong> </strong>
<label className="mr-3"> <div className="custom-controls-stacked">
<Field <Field
name="identifier" name="identifier"
component={renderField} component={renderRadioField}
type="radio" type="radio"
className="form-control mr-2" className="form-control mr-2"
value="ip" value="ip"
/>{' '} placeholder={t('ip_address')}
<Trans>ip_address</Trans> />
</label>
<label>
<Field <Field
name="identifier" name="identifier"
component={renderField} component={renderRadioField}
type="radio" type="radio"
className="form-control mr-2" className="form-control mr-2"
value="mac" value="mac"
/>{' '} placeholder="MAC"
MAC />
</label>
</div> </div>
</div>
<div className="row">
<div className="col col-sm-6">
{clientIdentifier === CLIENT_ID.IP && ( {clientIdentifier === CLIENT_ID.IP && (
<div className="form__group"> <div className="form__group">
<Field <Field
@ -77,6 +105,19 @@ let Form = (props) => {
/> />
</div> </div>
)} )}
</div>
<div className="col col-sm-6">
<Field
id="name"
name="name"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
validate={[required]}
/>
</div>
</div>
<div className="form__desc"> <div className="form__desc">
<Trans <Trans
components={[ components={[
@ -90,72 +131,67 @@ let Form = (props) => {
</div> </div>
</div> </div>
<div className="form__group"> <Tabs controlClass="form">
<div label="settings" title={props.t('main_settings')}>
{settingsCheckboxes.map(setting => (
<div className="form__group" key={setting.name}>
<Field <Field
id="name" name={setting.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" type="checkbox"
component={renderSelectField} component={renderSelectField}
placeholder={t('client_global_settings')} placeholder={t(setting.placeholder)}
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
/> />
</div> </div>
))}
</div>
<div label="services" title={props.t('block_services')}>
<div className="form__group"> <div className="form__group">
<Field <Field
name="filtering_enabled" name="use_global_blocked_services"
type="checkbox" type="checkbox"
component={renderSelectField} component={renderServiceField}
placeholder={t('block_domain_use_filters_and_hosts')} placeholder={t('blocked_services_global')}
disabled={useGlobalSettings} 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>
<div className="col-6">
<div className="form__group"> <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 <Field
name="safebrowsing_enabled" key={service.id}
icon={service.id}
name={`blocked_services.${service.id}`}
type="checkbox" type="checkbox"
component={renderSelectField} component={renderServiceField}
placeholder={t('use_adguard_browsing_sec')} placeholder={service.name}
disabled={useGlobalSettings} disabled={useGlobalServices}
/> />
))}
</div> </div>
<div className="form__group">
<Field
name="parental_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('use_adguard_parental')}
disabled={useGlobalSettings}
/>
</div> </div>
<div className="form__group">
<Field
name="safesearch_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div> </div>
</Tabs>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
@ -188,10 +224,12 @@ Form.propTypes = {
pristine: PropTypes.bool.isRequired, pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired, reset: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired, submitting: PropTypes.bool.isRequired,
toggleClientModal: PropTypes.func.isRequired, toggleClientModal: PropTypes.func.isRequired,
clientIdentifier: PropTypes.string, clientIdentifier: PropTypes.string,
useGlobalSettings: PropTypes.bool, useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired, processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired,
@ -202,9 +240,11 @@ const selector = formValueSelector('clientForm');
Form = connect((state) => { Form = connect((state) => {
const clientIdentifier = selector(state, 'identifier'); const clientIdentifier = selector(state, 'identifier');
const useGlobalSettings = selector(state, 'use_global_settings'); const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
return { return {
clientIdentifier, clientIdentifier,
useGlobalSettings, useGlobalSettings,
useGlobalServices,
}; };
})(Form); })(Form);

View File

@ -6,6 +6,24 @@ import ReactModal from 'react-modal';
import { MODAL_TYPE } from '../../../helpers/constants'; import { MODAL_TYPE } from '../../../helpers/constants';
import Form from './Form'; 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 Modal = (props) => {
const { const {
isModalOpen, isModalOpen,
@ -16,6 +34,7 @@ const Modal = (props) => {
processingAdding, processingAdding,
processingUpdating, processingUpdating,
} = props; } = props;
const initialData = getInitialData(currentClientData);
return ( return (
<ReactModal <ReactModal
@ -38,9 +57,7 @@ const Modal = (props) => {
</button> </button>
</div> </div>
<Form <Form
initialValues={{ initialValues={{ ...initialData }}
...currentClientData,
}}
onSubmit={handleSubmit} onSubmit={handleSubmit}
toggleClientModal={toggleClientModal} toggleClientModal={toggleClientModal}
processingAdding={processingAdding} processingAdding={processingAdding}

View 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;
}

View 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);

View 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);

View File

@ -11,11 +11,20 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
.form__inline {
display: flex;
justify-content: flex-start;
}
.btn-standard { .btn-standard {
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
} }
.btn-large {
min-width: 150px;
}
.form-control--textarea { .form-control--textarea {
min-height: 110px; min-height: 110px;
} }

View File

@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next'; import { withNamespaces, Trans } from 'react-i18next';
import Services from './Services';
import Checkbox from '../ui/Checkbox'; import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
@ -35,6 +36,7 @@ class Settings extends Component {
componentDidMount() { componentDidMount() {
this.props.initSettings(this.settings); this.props.initSettings(this.settings);
this.props.getBlockedServices();
} }
renderSettings = (settings) => { renderSettings = (settings) => {
@ -59,7 +61,9 @@ class Settings extends Component {
}; };
render() { render() {
const { settings, t } = this.props; const {
settings, services, setBlockedServices, t,
} = this.props;
return ( return (
<Fragment> <Fragment>
<PageTitle title={t('general_settings')} /> <PageTitle title={t('general_settings')} />
@ -74,6 +78,12 @@ class Settings extends Component {
</div> </div>
</Card> </Card>
</div> </div>
<div className="col-md-12">
<Services
services={services}
setBlockedServices={setBlockedServices}
/>
</div>
</div> </div>
</div> </div>
)} )}

View File

@ -63,6 +63,70 @@ const Icons = () => (
<symbol id="dns_privacy" viewBox="0 0 30 30" stroke="none" fill="currentColor" strokeLinecap="round" strokeLinejoin="round"> <symbol id="dns_privacy" viewBox="0 0 30 30" stroke="none" fill="currentColor" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 3C10.57 3 6.701 5.419 4.623 9h2.39a10.063 10.063 0 0 1 4.05-3.19c-.524.89-.961 1.973-1.3 3.19h2.108c.79-2.459 1.998-4 3.129-4s2.339 1.541 3.129 4h2.107c-.338-1.217-.774-2.3-1.299-3.19A10.062 10.062 0 0 1 22.989 9h2.389C23.298 5.419 19.43 3 15 3zm7.035 9.129c-1.372 0-2.264.73-2.264 1.842 0 .896.538 1.463 1.579 1.66l.75.15c.65.13.898.3.898.615 0 .375-.37.635-.91.635-.6 0-1.014-.265-1.049-.68h-1.38c.023 1.097.93 1.776 2.37 1.776 1.491 0 2.399-.717 2.399-1.904 0-.903-.504-1.412-1.63-1.63l-.734-.142c-.6-.118-.851-.3-.851-.611 0-.378.336-.62.844-.62.509 0 .891.28.923.682h1.336c-.024-1.053-.948-1.773-2.28-1.773zm-16.185.148v5.696h2.39c1.712 0 2.662-1.033 2.662-2.903 0-1.779-.966-2.793-2.662-2.793H5.85zm6.933.004v5.692h1.373v-3.235h.076l2.377 3.235h1.149V12.28h-1.373v3.203h-.076l-2.372-3.203h-1.154zm-5.486 1.16h.682c.912 0 1.449.596 1.449 1.657 0 1.128-.51 1.713-1.45 1.713h-.681v-3.37zM4.623 21C6.701 24.581 10.57 27 15 27c4.43 0 8.299-2.419 10.377-6h-2.389a10.063 10.063 0 0 1-4.049 3.19c.524-.89.96-1.973 1.297-3.19H18.13c-.79 2.459-1.996 4-3.127 4-1.131 0-2.339-1.541-3.129-4h-2.11c.339 1.217.776 2.3 1.3 3.19A10.056 10.056 0 0 1 7.013 21h-2.39z"></path> <path d="M15 3C10.57 3 6.701 5.419 4.623 9h2.39a10.063 10.063 0 0 1 4.05-3.19c-.524.89-.961 1.973-1.3 3.19h2.108c.79-2.459 1.998-4 3.129-4s2.339 1.541 3.129 4h2.107c-.338-1.217-.774-2.3-1.299-3.19A10.062 10.062 0 0 1 22.989 9h2.389C23.298 5.419 19.43 3 15 3zm7.035 9.129c-1.372 0-2.264.73-2.264 1.842 0 .896.538 1.463 1.579 1.66l.75.15c.65.13.898.3.898.615 0 .375-.37.635-.91.635-.6 0-1.014-.265-1.049-.68h-1.38c.023 1.097.93 1.776 2.37 1.776 1.491 0 2.399-.717 2.399-1.904 0-.903-.504-1.412-1.63-1.63l-.734-.142c-.6-.118-.851-.3-.851-.611 0-.378.336-.62.844-.62.509 0 .891.28.923.682h1.336c-.024-1.053-.948-1.773-2.28-1.773zm-16.185.148v5.696h2.39c1.712 0 2.662-1.033 2.662-2.903 0-1.779-.966-2.793-2.662-2.793H5.85zm6.933.004v5.692h1.373v-3.235h.076l2.377 3.235h1.149V12.28h-1.373v3.203h-.076l-2.372-3.203h-1.154zm-5.486 1.16h.682c.912 0 1.449.596 1.449 1.657 0 1.128-.51 1.713-1.45 1.713h-.681v-3.37zM4.623 21C6.701 24.581 10.57 27 15 27c4.43 0 8.299-2.419 10.377-6h-2.389a10.063 10.063 0 0 1-4.049 3.19c.524-.89.96-1.973 1.297-3.19H18.13c-.79 2.459-1.996 4-3.127 4-1.131 0-2.339-1.541-3.129-4h-2.11c.339 1.217.776 2.3 1.3 3.19A10.056 10.056 0 0 1 7.013 21h-2.39z"></path>
</symbol> </symbol>
<symbol id="youtube" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M19.695 4.04S15.348 3.2 12 3.2s-7.695.84-7.695.84L1.602 7.2v9.6l2.703 3.16s4.347.84 7.695.84 7.695-.84 7.695-.84l2.703-3.16V12 7.2zM9.602 15.68V8.32L16 12zm0 0"/><path d="M19.2 4a3.198 3.198 0 1 0 0 6.398c1.769 0 3.198-1.43 3.198-3.199C22.398 5.434 20.968 4 19.2 4zm0 9.602a3.198 3.198 0 1 0 0 6.398c1.769 0 3.198-1.434 3.198-3.2 0-1.769-1.43-3.198-3.199-3.198zM1.601 7.199c0 1.77 1.43 3.2 3.199 3.2 1.765 0 2.398-1.43 2.398-3.2C7.2 5.434 6.566 4 4.801 4 3.03 4 1.6 5.434 1.6 7.2zM4.8 13.602c-1.77 0-3.2 1.43-3.2 3.199A3.198 3.198 0 1 0 8 16.8c0-1.77-1.434-3.2-3.2-3.2zm0 0" />
</symbol>
<symbol id="discord" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M20.098 5.559C18.156 4 15.09 3.734 14.96 3.723a.493.493 0 0 0-.484.285c-.004.008-.172.504-.34.984 2.254.395 3.785 1.27 3.867 1.317a.8.8 0 1 1-.805 1.382C17.176 7.68 14.93 6.398 12 6.398c-2.93 0-5.176 1.282-5.2 1.293a.8.8 0 0 1-.805-1.383c.083-.046 1.622-.925 3.88-1.32-.172-.484-.348-.972-.352-.98a.487.487 0 0 0-.484-.285c-.129.011-3.195.273-5.16 1.855C2.852 6.528.8 12.074.8 16.871c0 .082.02.164.062.238 1.418 2.489 5.282 3.141 6.16 3.168h.016c.156 0 .3-.074.395-.199l.949-1.289c-2.086-.504-3.192-1.293-3.258-1.344a.799.799 0 0 1-.168-1.117.794.794 0 0 1 1.113-.172c.032.016 2.067 1.446 5.93 1.446 3.879 0 5.91-1.434 5.93-1.45a.8.8 0 0 1 .945 1.293c-.066.047-1.164.836-3.246 1.34l.937 1.293c.094.125.239.2.395.2h.016c.882-.028 4.742-.68 6.16-3.169a.477.477 0 0 0 .062-.242c0-4.793-2.05-10.34-3.101-11.308zM8.8 15.199c-.887 0-1.602-.894-1.602-2 0-1.105.715-2 1.602-2 .883 0 1.597.895 1.597 2 0 1.106-.714 2-1.597 2zm6.398 0c-.883 0-1.597-.894-1.597-2 0-1.105.714-2 1.597-2 .887 0 1.602.895 1.602 2 0 1.106-.715 2-1.602 2zm0 0"/>
</symbol>
<symbol id="twitch" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M4.8 3.2L3.2 6.397V19.2h4v2.403h3.198l2.403-2.403H16l4.8-4.8v-11.2zm14.4 10.402L16.8 16H12l-2.398 2.398V16H6.398V4.8H19.2zm0 0" /><path d="M15.2 12.8h-1.598V7.2h1.597zm-3.2 0h-1.602V7.2H12zm0 0" />
</symbol>
<symbol id="messenger" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M5.602 22.398L10.398 20l-4.796-2.398zm0 0" /><path d="M12 2.398c-5.3 0-9.602 4.122-9.602 9.204C2.398 16.68 6.7 20.8 12 20.8c5.3 0 9.602-4.121 9.602-9.2 0-5.081-4.301-9.203-9.602-9.203zm.91 11.844l-2.305-2.48-4.328 2.422 4.813-5.098 2.36 2.363 4.218-2.363zm0 0" />
</symbol>
<symbol id="snapchat" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M12.176 4c.715 0 3.136.191 4.277 2.668.383.828.285 2.273.211 3.437l-.004.051c-.008.164-.02.32-.027.469.015.02.164.156.492.168.25-.012.54-.086.855-.23a.784.784 0 0 1 .57.008h.005c.254.09.422.261.425.44.004.173-.128.43-.789.68a2.694 2.694 0 0 1-.25.082c-.375.118-.945.293-1.117.692-.097.215-.066.48.09.785 0 .004.004.008.004.012.047.105 1.187 2.62 3.73 3.027.094.016.16.094.153.188a.24.24 0 0 1-.024.101c-.105.238-.578.574-2.234.824-.133.02-.188.188-.266.547-.03.13-.058.258-.101.39-.035.118-.11.173-.235.173h-.02a2.34 2.34 0 0 1-.37-.043 4.986 4.986 0 0 0-.996-.102c-.23 0-.473.02-.715.059-.496.078-.918.367-1.363.672-.653.445-1.32.902-2.364.902-.047 0-.09 0-.136-.004-.028.004-.055.004-.086.004-1.043 0-1.711-.457-2.36-.902-.445-.305-.867-.594-1.363-.672a4.533 4.533 0 0 0-.719-.059c-.418 0-.75.063-.992.106a2.02 2.02 0 0 1-.371.054c-.102 0-.211-.023-.258-.18-.039-.136-.07-.269-.101-.394-.075-.328-.125-.531-.266-.55-1.656-.247-2.129-.587-2.234-.825-.012-.035-.024-.066-.024-.101a.182.182 0 0 1 .156-.188c2.54-.406 3.68-2.922 3.727-3.031.004 0 .004-.004.004-.008.156-.305.187-.57.094-.79-.176-.398-.747-.57-1.122-.687a3.147 3.147 0 0 1-.25-.082c-.75-.289-.812-.582-.785-.734.051-.254.407-.434.692-.434a.49.49 0 0 1 .207.04c.336.152.64.23.906.23.363 0 .52-.148.54-.168-.009-.164-.02-.34-.032-.52-.074-1.164-.168-2.609.21-3.433 1.138-2.477 3.555-2.668 4.27-2.668L12.133 4h.043m0-1.602h-.043l-.313.008v-.004c-.953 0-4.187.262-5.722 3.598-.387.844-.45 1.887-.422 2.922-.922.02-2 .625-2.215 1.726-.082.407-.184 1.786 1.781 2.54.012.003.02.007.031.011-.39.559-1.113 1.34-2.168 1.508-.902.14-1.55.941-1.5 1.86.016.226.067.44.153.64.41.938 1.406 1.363 2.543 1.613a1.83 1.83 0 0 0 1.785 1.305c.246 0 .465-.043.66-.078a3.44 3.44 0 0 1 .703-.082c.149 0 .305.012.465.039.14.023.457.238.711.41.73.5 1.727 1.184 3.266 1.184h.101c.04 0 .078.004.121.004 1.532 0 2.528-.68 3.258-1.176.281-.192.582-.399.723-.422.156-.024.312-.04.46-.04.259 0 .458.032.696.075.266.05.477.074.668.074.852 0 1.543-.508 1.785-1.293 1.137-.25 2.129-.672 2.535-1.593.094-.22.149-.43.16-.649a1.783 1.783 0 0 0-1.496-1.871c-1.054-.168-1.78-.95-2.172-1.508l.036-.011c1.601-.618 1.824-1.645 1.816-2.204-.02-.855-.594-1.601-1.477-1.918a2.37 2.37 0 0 0-.777-.156c.027-1.015-.039-2.078-.422-2.914-1.539-3.336-4.773-3.598-5.73-3.598zm0 0" />
</symbol>
<symbol id="twitter" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M22.398 5.55a8.583 8.583 0 0 1-2.449.673 4.252 4.252 0 0 0 1.875-2.364 8.66 8.66 0 0 1-2.71 1.04A4.251 4.251 0 0 0 16 3.546a4.27 4.27 0 0 0-4.266 4.27c0 .335.036.66.11.972a12.126 12.126 0 0 1-8.797-4.46 4.259 4.259 0 0 0-.578 2.148c0 1.48.754 2.785 1.898 3.55a4.273 4.273 0 0 1-1.933-.535v.055a4.27 4.27 0 0 0 3.425 4.183c-.359.098-.734.149-1.125.149-.273 0-.543-.027-.804-.074a4.276 4.276 0 0 0 3.988 2.965 8.562 8.562 0 0 1-5.3 1.824 8.82 8.82 0 0 1-1.02-.059 12.088 12.088 0 0 0 6.543 1.918c7.851 0 12.14-6.504 12.14-12.144 0-.184-.004-.368-.011-.551a8.599 8.599 0 0 0 2.128-2.207zm0 0" />
</symbol>
<symbol id="instagram" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M12 8.8A3.2 3.2 0 0 0 8.8 12a3.2 3.2 0 0 0 3.2 3.2 3.2 3.2 0 0 0 3.2-3.2A3.2 3.2 0 0 0 12 8.8zm0 0" /><path d="M16 2.398H8A5.609 5.609 0 0 0 2.398 8v8A5.609 5.609 0 0 0 8 21.602h8A5.609 5.609 0 0 0 21.602 16V8A5.609 5.609 0 0 0 16 2.398zm-4 14.403A4.805 4.805 0 0 1 7.2 12c0-2.648 2.152-4.8 4.8-4.8 2.648 0 4.8 2.152 4.8 4.8 0 2.648-2.152 4.8-4.8 4.8zm5.602-9.602a.799.799 0 1 1 0 0zm0 0" />
</symbol>
<symbol id="whatsapp" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M3.836 16.668l-1.352 4.934 5.047-1.329zm0 0" /><path d="M12 2.398C6.7 2.398 2.398 6.7 2.398 12c0 5.3 4.301 9.602 9.602 9.602 5.3 0 9.602-4.301 9.602-9.602 0-5.3-4.301-9.602-9.602-9.602zm4.738 12.915c-.195.554-1.168 1.093-1.601 1.128-.442.043-.852.2-2.856-.59-2.418-.953-3.945-3.433-4.062-3.593-.121-.156-.969-1.285-.969-2.453 0-1.172.613-1.746.828-1.985a.875.875 0 0 1 .637-.297c.156 0 .316 0 .453.004.172.004.36.016.535.41.215.47.676 1.645.735 1.766.058.117.101.262.019.418-.078.156-.121.254-.234.399-.121.136-.25.308-.36.41-.117.12-.242.25-.101.488.136.238.613 1.016 1.32 1.645.906.812 1.672 1.062 1.91 1.18.238.12.38.1.516-.06.14-.156.594-.69.754-.93.156-.237.316-.198.531-.12.219.078 1.39.656 1.629.773.238.121.394.18.453.278.063.097.063.574-.137 1.129zm0 0" />
</symbol>
<symbol id="facebook" viewBox="0 0 27 27" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M12 0C5.371 0 0 5.371 0 12c0 6.016 4.434 10.984 10.207 11.852V15.18H7.238v-3.153h2.969V9.926c0-3.473 1.691-5 4.578-5 1.387 0 2.117.105 2.461.148v2.754h-1.969c-1.226 0-1.652 1.164-1.652 2.473v1.726h3.594l-.489 3.153h-3.105v8.699C19.48 23.082 24 18.074 24 12c0-6.629-5.371-12-12-12zm0 0" />
</symbol>
<symbol id="netflix" viewBox="0 0 450 600" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M83.5 72.814V512l17.432-2.865a955.35 955.35 0 0 1 88.604-10.312l13.965-.966V338.206L83.5 72.814z"/><path d="M308.5 0L308.5 172.328 428.5 438.914 428.5 0z"/><path d="M308.5 245.415l-10.87-24.149L198.03 0H83.501l168.12 371.813 57.024 126.112 8.852.566a955.65 955.65 0 0 1 93.572 10.644L428.5 512l-120-266.585z"/>
</symbol>
<symbol id="vk" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M12 .96C5.914.96.96 5.915.96 12c0 6.086 4.954 11.04 11.04 11.04 6.086 0 11.04-4.954 11.04-11.04C23.04 5.914 18.085.96 12 .96zm4.785 13.216c1.074.953 1.3 1.293 1.336 1.351.445.707-.492.793-.492.793h-1.98s-.481.004-.891-.27c-.672-.437-1.375-1.288-1.867-1.14-.414.125-.41.684-.41 1.16 0 .172-.149.25-.481.25h-.617c-1.086 0-2.262-.363-3.434-1.59-1.656-1.734-3.113-5.222-3.113-5.222s-.086-.176.008-.281c.105-.122.394-.106.394-.106h1.918s.18.031.309.125c.11.074.168.219.168.219s.32 1.062.734 1.742c.801 1.32 1.172 1.355 1.445 1.215.399-.207.266-1.617.266-1.617s.02-.602-.187-.871c-.16-.211-.465-.32-.598-.336-.11-.016.07-.203.3-.313.31-.137.727-.172 1.446-.164.563.004.723.04.941.09.665.152.5.555.5 1.969 0 .453-.062 1.09.278 1.3.148.09.652.204 1.55-1.257.43-.692.77-1.84.77-1.84s.067-.125.176-.188c.113-.066.11-.062.262-.062.152 0 1.683-.012 2.02-.012.335 0 .651-.004.702.191.078.282-.246 1.25-1.07 2.305-1.355 1.723-1.504 1.563-.383 2.559zm0 0" />
</symbol>
<symbol id="ok" viewBox="0 0 96 96" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M50 28c-3.313 0-6 2.688-6 6 0 3.313 2.688 6 6 6 3.313 0 6-2.688 6-6 0-3.313-2.688-6-6-6zm0 0" /><path d="M50 4C24.637 4 4 24.637 4 50s20.637 46 46 46 46-20.637 46-46S75.363 4 50 4zm0 16c7.73 0 14 6.27 14 14s-6.27 14-14 14-14-6.27-14-14 6.27-14 14-14zm14.828 49.172A3.999 3.999 0 0 1 62 76a3.987 3.987 0 0 1-2.828-1.172L50 65.656l-9.172 9.172a3.999 3.999 0 0 1-5.656 0 3.999 3.999 0 0 1 0-5.656l6.43-6.43c-1.836-.539-3.618-1.207-5.29-2.066A4.302 4.302 0 0 1 34 56.859c0-2.98 3.172-4.761 5.809-3.375A21.767 21.767 0 0 0 50 56c3.684 0 7.148-.91 10.191-2.516C62.828 52.098 66 53.88 66 56.86c0 1.602-.89 3.078-2.313 3.813-1.671.863-3.453 1.531-5.289 2.07zm0 0" />
</symbol>
<symbol id="steam" viewBox="0 0 22 22" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M14.398 7.2a2.4 2.4 0 1 0 .003 4.799 2.4 2.4 0 0 0-.003-4.8zm0 0" fill="none" strokeWidth="1.6" stroke="currentColor" strokeMiterlimit="10"/><path d="M8 14c-.629 0-1.18.297-1.547.75l1.758.48c.426.114.68.555.562.98a.804.804 0 0 1-.984.563l-1.762-.48A1.998 1.998 0 0 0 10 16c0-1.105-.895-2-2-2zm0 0" /><path d="M19.2 3.2H4.8c-.886 0-1.6.714-1.6 1.6v9.063l2.027.551a3.213 3.213 0 0 1 2.289-1.566l2.136-2.567a4.799 4.799 0 1 1 4.066 4.066l-2.566 2.137A3.195 3.195 0 0 1 8 19.2 3.2 3.2 0 0 1 4.8 16c0-.016.005-.027.005-.043l-1.606-.437v3.68c0 .886.715 1.6 1.602 1.6h14.398c.887 0 1.602-.714 1.602-1.6V4.8c0-.886-.715-1.6-1.602-1.6zm0 0" />
</symbol>
<symbol id="skype" viewBox="0 0 26 26" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M23.363 14.387c.153-.739.23-1.5.23-2.266C23.594 5.883 18.45.805 12.122.805c-.594 0-1.191.047-1.781.136A6.891 6.891 0 0 0 6.852 0C3.074 0 0 3.035 0 6.762c0 1.144.293 2.27.852 3.265-.133.688-.2 1.391-.2 2.094 0 6.238 5.149 11.316 11.47 11.316.648 0 1.3-.054 1.94-.164.95.477 2.012.727 3.086.727C20.926 24 24 20.969 24 17.238c0-1.004-.215-1.96-.637-2.851zM17.758 17.3c-.508.707-1.258 1.27-2.23 1.668-.966.394-2.122.593-3.434.593-1.578 0-2.903-.273-3.934-.812a5.074 5.074 0 0 1-1.808-1.582c-.47-.664-.707-1.324-.707-1.961 0-.395.156-.738.457-1.023.304-.278.687-.418 1.148-.418.379 0 .703.109.969.332.254.21.469.523.644.93.192.437.407.808.633 1.1.211.282.524.52.918.704.399.188.938.281 1.598.281.91 0 1.652-.191 2.215-.57.546-.367.812-.813.812-1.352 0-.43-.14-.765-.422-1.027-.3-.277-.699-.492-1.176-.637-.5-.152-1.18-.32-2.015-.496-1.14-.238-2.11-.523-2.88-.847-.788-.332-1.425-.79-1.89-1.364-.472-.582-.71-1.312-.71-2.172 0-.816.253-1.554.75-2.191.488-.633 1.206-1.125 2.132-1.46.91-.333 1.996-.5 3.223-.5.98 0 1.844.108 2.566.331.723.223 1.336.524 1.813.89.484.376.843.774 1.07 1.188.227.418.344.832.344 1.235 0 .386-.153.738-.453 1.046-.297.31-.68.465-1.125.465-.41 0-.727-.097-.95-.289-.207-.18-.418-.46-.656-.863-.273-.516-.605-.918-.984-1.203-.371-.277-.989-.418-1.836-.418-.79 0-1.43.156-1.902.465-.461.293-.684.633-.684 1.039 0 .246.07.449.219.629.156.187.379.351.656.488.289.145.586.258.883.34.308.082.82.207 1.523.367.887.191 1.707.398 2.43.625.73.234 1.363.516 1.879.848.527.34.941.773 1.238 1.293.297.52.445 1.16.445 1.91a4.07 4.07 0 0 1-.77 2.418zm0 0"/>
</symbol>
<symbol id="mail_ru" viewBox="0 0 512 512" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M256 141.176c-63.306 0-114.809 51.503-114.809 114.809S192.694 370.795 256 370.795s114.809-51.503 114.809-114.809S319.306 141.176 256 141.176zm0 188.254c-40.498 0-73.445-32.947-73.445-73.445 0-40.498 32.947-73.445 73.445-73.445 40.499 0 73.445 32.947 73.445 73.445 0 40.498-32.946 73.445-73.445 73.445z"/><path d="M437.008 74.97C388.656 26.623 324.375 0 256 0h-.017C187.603.004 123.318 26.637 74.97 74.992 26.62 123.347-.005 187.637 0 256.017c.004 68.379 26.637 132.666 74.992 181.014C123.344 485.377 187.625 512.001 256 512h.017c55.945-.004 111.216-18.738 155.631-52.752 9.07-6.945 10.792-19.927 3.846-28.995-6.945-9.069-19.926-10.794-28.995-3.846-37.24 28.518-83.58 44.224-130.486 44.228h-.014c-57.324 0-111.224-22.324-151.761-62.856-40.542-40.536-62.871-94.435-62.875-151.766-.006-118.35 96.273-214.641 214.623-214.649H256c118.34 0 214.628 96.279 214.636 214.622v23.532c0 27.523-22.39 49.913-49.913 49.913-27.523 0-49.913-22.391-49.913-49.913v-23.532c0-11.422-9.259-20.682-20.682-20.682s-20.682 9.26-20.682 20.682v23.532c0 50.33 40.947 91.278 91.278 91.278S512 329.848 512 279.518v-23.534c-.005-68.38-26.638-132.666-74.992-181.014z"/>
</symbol>
<symbol id="question" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><line x1="12" y1="17" x2="12" y2="17" />
</symbol>
</svg> </svg>
); );

View File

@ -109,9 +109,11 @@
width: 20px; width: 20px;
height: 20px; height: 20px;
stroke: #9aa0ac; stroke: #9aa0ac;
color: #9aa0ac;
} }
.popover__icon--green { .popover__icon--green {
color: #66b574;
stroke: #66b574; stroke: #66b574;
} }

View File

@ -6,19 +6,36 @@ import './Popover.css';
class PopoverFilter extends Component { class PopoverFilter extends Component {
render() { render() {
const { rule, filter, service } = this.props;
if (!rule && !service) {
return '';
}
return ( return (
<div className="popover-wrap"> <div className="popover-wrap">
<div className="popover__trigger popover__trigger--filter"> <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>
<div className="popover__body popover__body--filter"> <div className="popover__body popover__body--filter">
<div className="popover__list"> <div className="popover__list">
{rule && (
<div className="popover__list-item popover__list-item--nowrap"> <div className="popover__list-item popover__list-item--nowrap">
<Trans>rule_label</Trans>: <strong>{this.props.rule}</strong> <Trans>rule_label</Trans>: <strong>{rule}</strong>
</div> </div>
{this.props.filter && <div className="popover__list-item popover__list-item--nowrap"> )}
<Trans>filter_label</Trans>: <strong>{this.props.filter}</strong> {filter && (
</div>} <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> </div>
</div> </div>
@ -27,8 +44,9 @@ class PopoverFilter extends Component {
} }
PopoverFilter.propTypes = { PopoverFilter.propTypes = {
rule: PropTypes.string.isRequired, rule: PropTypes.string,
filter: PropTypes.string, filter: PropTypes.string,
service: PropTypes.string,
}; };
export default withNamespaces()(PopoverFilter); export default withNamespaces()(PopoverFilter);

View File

@ -6,6 +6,20 @@
border-bottom: 1px solid #e8e8e8; 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 { .tab__control {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import Tab from './Tab'; import Tab from './Tab';
import './Tabs.css'; import './Tabs.css';
@ -16,6 +17,7 @@ class Tabs extends Component {
render() { render() {
const { const {
props: { props: {
controlClass,
children, children,
}, },
state: { state: {
@ -23,9 +25,14 @@ class Tabs extends Component {
}, },
} = this; } = this;
const getControlClass = classnames({
tabs__controls: true,
[`tabs__controls--${controlClass}`]: controlClass,
});
return ( return (
<div className="tabs"> <div className="tabs">
<div className="tabs__controls"> <div className={getControlClass}>
{children.map((child) => { {children.map((child) => {
const { label, title } = child.props; const { label, title } = child.props;
@ -54,6 +61,7 @@ class Tabs extends Component {
} }
Tabs.propTypes = { Tabs.propTypes = {
controlClass: PropTypes.string,
children: PropTypes.array.isRequired, children: PropTypes.array.isRequired,
}; };

View File

@ -1,11 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { initSettings, toggleSetting } from '../actions'; import { initSettings, toggleSetting } from '../actions';
import { getBlockedServices, setBlockedServices } from '../actions/services';
import Settings from '../components/Settings'; import Settings from '../components/Settings';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { settings } = state; const { settings, services } = state;
const props = { const props = {
settings, settings,
services,
}; };
return props; return props;
}; };
@ -13,6 +15,8 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = { const mapDispatchToProps = {
initSettings, initSettings,
toggleSetting, toggleSetting,
getBlockedServices,
setBlockedServices,
}; };
export default connect( export default connect(

View File

@ -181,3 +181,66 @@ export const CLIENT_ID = {
}; };
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients']; 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',
},
];

View File

@ -27,6 +27,23 @@ export const renderField = ({
</Fragment> </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 = ({ export const renderSelectField = ({
input, placeholder, disabled, meta: { touched, error }, input, placeholder, disabled, meta: { touched, error },
}) => ( }) => (
@ -46,6 +63,28 @@ export const renderSelectField = ({
</Fragment> </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) => { export const required = (value) => {
if (value || value === 0) { if (value || value === 0) {
return false; return false;

View File

@ -27,6 +27,7 @@ export const normalizeLogs = logs => logs.map((log) => {
client, client,
filterId, filterId,
rule, rule,
service_name,
} = log; } = log;
const { host: domain, type } = question; const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => { const responsesArray = response ? response.map((response) => {
@ -42,6 +43,7 @@ export const normalizeLogs = logs => logs.map((log) => {
client, client,
filterId, filterId,
rule, rule,
serviceName: service_name,
}; };
}); });
@ -225,3 +227,7 @@ export const sortClients = (clients) => {
return clients.sort(compare); return clients.sort(compare);
}; };
export const toggleAllServices = (services, change, isSelected) => {
services.forEach(service => change(`blocked_services.${service.id}`, isSelected));
};

View File

@ -10,6 +10,7 @@ import encryption from './encryption';
import clients from './clients'; import clients from './clients';
import access from './access'; import access from './access';
import rewrites from './rewrites'; import rewrites from './rewrites';
import services from './services';
const settings = handleActions({ const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }), [actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@ -424,6 +425,7 @@ export default combineReducers({
clients, clients,
access, access,
rewrites, rewrites,
services,
loadingBar: loadingBarReducer, loadingBar: loadingBarReducer,
form: formReducer, form: formReducer,
}); });

View 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;