Files
AdGuardHome/client/src/components/Settings/Clients/Form.js
Dimitry Kolyshev df61741f60 Pull request: 1163 safesearch http api vol.3
Merge in DNS/adguard-home from 1163-safesearch-1-3 to master

Squashed commit of the following:

commit f26c5fb4f7a27dc61b10c28d6672d5307796784c
Merge: e7a1b885 143616ca
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 23 18:45:25 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-3

    # Conflicts:
    #	CHANGELOG.md

commit e7a1b885d67628c187ce08327338cf3138012f03
Merge: 01b73d76 eb5d8a49
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Wed Mar 22 13:55:23 2023 +0200

    Merge branch '1163-safesearch-1-3' of ssh://bit.adguard.com:7999/dns/adguard-home into 1163-safesearch-1-3

commit 01b73d763c6ee76de995093cc7107f113c7785ce
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Wed Mar 22 13:52:02 2023 +0200

    client: add safe search extended settings to clients

commit eb5d8a499ac1036e5077d2b4aa84479381577e10
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 22 18:50:23 2023 +0700

    all: docs

commit 2043a8fba7f664ef365ccc5abac14a85035eb4b7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 22 09:42:50 2023 +0700

    all: docs

commit bb1d2f6c0252891ccac3d3727eb23288a24d4bda
Merge: 95f9fd3d c3edab43
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 22 09:42:00 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-3

commit 95f9fd3dd1e8abcdf1a156e81aff8e52f320f4c3
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Tue Mar 21 15:25:39 2023 +0200

    client: move to new safe search api

commit ac823a911f0d6ab6f1813d11a0ca082d54cc9131
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:40:29 2023 +0700

    all: docs

commit aaa287b125c7c7a775b821e0dd272199229a7538
Merge: 16fa7031 48431f8b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:39:14 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-3

commit 16fa7031ab2aec31139ace54ffa0155cde8e9135
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:39:03 2023 +0700

    all: docs

commit 498f7d3cbb842eda218b0fd06fc3bb3601b81f80
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 18:59:47 2023 +0700

    filtering: imp code

commit aab7b70e2355ba86577e5156c1d5569b21b4b358
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 18:40:18 2023 +0700

    filtering: imp code

commit d2870a18ffdb1d293993487073912168d6b75a38
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 17 21:57:58 2023 +0700

    filtering: imp code

commit 868f5d1ed29c3af702114079e7ffe46e136eb901
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 17 19:06:36 2023 +0700

    all: imp docs

commit f6d70b06ed873684501ce17f647ccf07a85dd50b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 17 19:05:40 2023 +0700

    filtering: imp code

commit 7cd9a37dde6262a8cf4f0f13f9946e011cc0e2cf
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 16 14:56:51 2023 +0700

    home: imp code

commit 84d8817512e47a517ed2880ffa9dde5ffda1d288
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 16 09:39:41 2023 +0700

    all: safesearch http api
2023-03-23 15:25:58 +03:00

405 lines
14 KiB
JavaScript

import React, { useState } from 'react';
import { connect, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
Field, FieldArray, reduxForm, formValueSelector,
} from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import Select from 'react-select';
import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
import {
renderInputField,
renderGroupField,
CheckboxField,
renderServiceField,
renderTextareaField,
} from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME } 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',
},
];
const validate = (values) => {
const errors = {};
const { name, ids } = values;
errors.name = validateRequiredValue(name);
if (ids && ids.length) {
const idArrayErrors = [];
ids.forEach((id, idx) => {
idArrayErrors[idx] = validateRequiredValue(id) || validateClientId(id);
});
if (idArrayErrors.length) {
errors.ids = idArrayErrors;
}
}
return errors;
};
const renderFieldsWrapper = (placeholder, buttonTitle) => function cell(row) {
const {
fields,
} = row;
return (
<div className="form__group">
{fields.map((ip, index) => (
<div key={index} className="mb-1">
<Field
name={ip}
component={renderGroupField}
type="text"
className="form-control"
placeholder={placeholder}
isActionAvailable={index !== 0}
removeField={() => fields.remove(index)}
normalizeOnBlur={(data) => data.trim()}
/>
</div>
))}
<button
type="button"
className="btn btn-link btn-block btn-sm"
onClick={() => fields.push()}
title={buttonTitle}
>
<svg className="icon icon--24">
<use xlinkHref="#plus" />
</svg>
</button>
</div>
);
};
// Should create function outside of component to prevent component re-renders
const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id'));
const renderMultiselect = (props) => {
const { input, placeholder, options } = props;
return (
<Select
{...input}
options={options}
className="basic-multi-select"
classNamePrefix="select"
onChange={(value) => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
placeholder={placeholder}
blurInputOnSelect={false}
isMulti
/>
);
};
renderMultiselect.propTypes = {
input: PropTypes.object.isRequired,
placeholder: PropTypes.string,
options: PropTypes.array,
};
let Form = (props) => {
const {
t,
handleSubmit,
reset,
change,
pristine,
submitting,
useGlobalSettings,
useGlobalServices,
toggleClientModal,
processingAdding,
processingUpdating,
invalid,
tagsOptions,
initialValues,
} = props;
const services = useSelector((store) => store?.services);
const { safe_search } = initialValues;
const safeSearchServices = { ...safe_search };
delete safeSearchServices.enabled;
const [activeTabLabel, setActiveTabLabel] = useState('settings');
const tabs = {
settings: {
title: 'settings',
component: <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={CheckboxField}
placeholder={t(setting.placeholder)}
disabled={
setting.name !== 'use_global_settings'
? useGlobalSettings
: false
}
/>
</div>
))}
<div className="form__group">
<Field
name="safe_search.enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div>
<div className='form__group--inner'>
{Object.keys(safeSearchServices).map((searchKey) => (
<div key={searchKey}>
<Field
name={`safe_search.${searchKey}`}
type="checkbox"
component={CheckboxField}
placeholder={captitalizeWords(searchKey)}
disabled={useGlobalSettings}
/>
</div>
))}
</div>
</div>,
},
block_services: {
title: 'block_services',
component: <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.allServices, 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.allServices, change, false)
)}
>
<Trans>unblock_all</Trans>
</button>
</div>
</div>
{services.allServices.length > 0 && (
<div className="services">
{services.allServices.map((service) => (
<Field
key={service.id}
icon={service.icon_svg}
name={`blocked_services.${service.id}`}
type="checkbox"
component={renderServiceField}
placeholder={service.name}
disabled={useGlobalServices}
/>
))}
</div>
)}
</div>
</div>,
},
upstream_dns: {
title: 'upstream_dns',
component: <div label="upstream" title={props.t('upstream_dns')}>
<div className="form__desc mb-3">
<Trans components={[<a href="#dns" key="0">link</a>]}>
upstream_dns_client_desc
</Trans>
</div>
<Field
id="upstreams"
name="upstreams"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')}
normalizeOnBlur={trimLinesAndRemoveEmpty}
/>
<Examples />
</div>,
},
};
const activeTab = tabs[activeTabLabel].component;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group mb-0">
<div className="form__group">
<Field
id="name"
name="name"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
normalizeOnBlur={(data) => data.trim()}
/>
</div>
<div className="form__group mb-4">
<div className="form__label">
<strong className="mr-3">
<Trans>tags_title</Trans>
</strong>
</div>
<div className="form__desc mt-0 mb-2">
<Trans components={[
<a target="_blank" rel="noopener noreferrer" href="https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#ctag"
key="0">link</a>,
]}>
tags_desc
</Trans>
</div>
<Field
name="tags"
component={renderMultiselect}
placeholder={t('form_select_tags')}
options={tagsOptions}
/>
</div>
<div className="form__group">
<div className="form__label">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
</div>
<div className="form__desc mt-0">
<Trans components={[
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer"
key="0">text</a>,
]}>
client_identifier_desc
</Trans>
</div>
</div>
<div className="form__group">
<FieldArray
name="ids"
component={renderFields}
/>
</div>
</div>
<Tabs controlClass="form" tabs={tabs} activeTabLabel={activeTabLabel}
setActiveTabLabel={setActiveTabLabel}>
{activeTab}
</Tabs>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={() => {
reset();
toggleClientModal();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| pristine
|| processingAdding
|| processingUpdating
}
>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
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,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired,
initialValues: PropTypes.object,
};
const selector = formValueSelector(FORM_NAME.CLIENT);
Form = connect((state) => {
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
return {
useGlobalSettings,
useGlobalServices,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.CLIENT,
enableReinitialize: true,
validate,
}),
])(Form);