all: sync with master

This commit is contained in:
Ainar Garipov
2024-07-03 15:38:37 +03:00
parent f73717ec08
commit 158d4f0249
352 changed files with 33842 additions and 33276 deletions

View File

@@ -1,20 +1,30 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import Card from '../../ui/Card';
import CellWrap from '../../ui/CellWrap';
import whoisCell from './whoisCell';
import LogsSearchLink from '../../ui/LogsSearchLink';
import { sortIp } from '../../../helpers/helpers';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
import { TABLES_MIN_ROWS } from '../../../helpers/constants';
const COLUMN_MIN_WIDTH = 200;
class AutoClients extends Component {
interface AutoClientsProps {
t: (...args: unknown[]) => string;
autoClients: any[];
normalizedTopClients: any;
}
class AutoClients extends Component<AutoClientsProps> {
columns = [
{
Header: this.props.t('table_client'),
@@ -39,24 +49,24 @@ class AutoClients extends Component {
Header: this.props.t('whois'),
accessor: 'whois_info',
minWidth: COLUMN_MIN_WIDTH,
Cell: whoisCell(this.props.t),
},
{
Header: this.props.t('requests_count'),
accessor: (row) => this.props.normalizedTopClients.auto[row.ip] || 0,
sortMethod: (a, b) => b - a,
accessor: (row: any) => this.props.normalizedTopClients.auto[row.ip] || 0,
sortMethod: (a: any, b: any) => b - a,
id: 'statistics',
minWidth: COLUMN_MIN_WIDTH,
Cell: (row) => {
Cell: (row: any) => {
const { value: clientStats } = row;
if (clientStats) {
return (
<div className="logs__row">
<div className="logs__text" title={clientStats}>
<LogsSearchLink search={row.original.ip}>
{clientStats}
</LogsSearchLink>
<LogsSearchLink search={row.original.ip}>{clientStats}</LogsSearchLink>
</div>
</div>
);
@@ -74,8 +84,7 @@ class AutoClients extends Component {
<Card
title={t('auto_clients_title')}
subtitle={t('auto_clients_desc')}
bodyType="card-body box-body--settings"
>
bodyType="card-body box-body--settings">
<ReactTable
data={autoClients || []}
columns={this.columns}
@@ -88,9 +97,9 @@ class AutoClients extends Component {
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE) || 10}
onPageSizeChange={(size) => (
onPageSizeChange={(size: any) =>
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE, size)
)}
}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
@@ -105,10 +114,4 @@ class AutoClients extends Component {
}
}
AutoClients.propTypes = {
t: PropTypes.func.isRequired,
autoClients: PropTypes.array.isRequired,
normalizedTopClients: PropTypes.object.isRequired,
};
export default withTranslation()(AutoClients);

View File

@@ -1,26 +1,46 @@
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
import { initSettings } from '../../../../actions';
import {
splitByNewLine,
countClientsStatistics,
sortIp,
getService,
} from '../../../../helpers/helpers';
import { splitByNewLine, countClientsStatistics, sortIp, getService } from '../../../../helpers/helpers';
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';
import Card from '../../../ui/Card';
import CellWrap from '../../../ui/CellWrap';
import LogsSearchLink from '../../../ui/LogsSearchLink';
import Modal from '../Modal';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../../helpers/localStorageHelper';
import { Client, NormalizedTopClients, RootState } from '../../../../initialState';
interface ClientsTableProps {
clients: Client[];
normalizedTopClients: NormalizedTopClients;
toggleClientModal: (...args: unknown[]) => unknown;
deleteClient: (...args: unknown[]) => string;
addClient: (...args: unknown[]) => string;
updateClient: (...args: unknown[]) => string;
isModalOpen: boolean;
modalType: string;
modalClientName: string;
processingAdding: boolean;
processingDeleting: boolean;
processingUpdating: boolean;
getStats: (...args: unknown[]) => unknown;
supportedTags: string[];
}
const ClientsTable = ({
clients,
@@ -37,18 +57,18 @@ const ClientsTable = ({
processingUpdating,
getStats,
supportedTags,
}) => {
}: ClientsTableProps) => {
const [t] = useTranslation();
const dispatch = useDispatch();
const location = useLocation();
const history = useHistory();
const services = useSelector((store) => store?.services);
const globalSettings = useSelector((store) => store?.settings.settingsList) || {};
const services = useSelector((state: RootState) => state?.services);
const globalSettings = useSelector((state: RootState) => state?.settings.settingsList);
const params = new URLSearchParams(location.search);
const clientId = params.get('clientId');
const { safesearch } = globalSettings;
useEffect(() => {
dispatch(getAllBlockedServices());
dispatch(getBlockedServices());
@@ -61,22 +81,22 @@ const ClientsTable = ({
}
}, []);
const handleFormAdd = (values) => {
const handleFormAdd = (values: any) => {
addClient(values);
};
const handleFormUpdate = (values, name) => {
const handleFormUpdate = (values: any, name: any) => {
updateClient(values, name);
};
const handleSubmit = (values) => {
const handleSubmit = (values: any) => {
const config = { ...values };
if (values) {
if (values.blocked_services) {
config.blocked_services = Object
.keys(values.blocked_services)
.filter((service) => values.blocked_services[service]);
config.blocked_services = Object.keys(values.blocked_services).filter(
(service) => values.blocked_services[service],
);
}
if (values.upstreams && typeof values.upstreams === 'string') {
@@ -86,7 +106,7 @@ const ClientsTable = ({
}
if (values.tags) {
config.tags = values.tags.map((tag) => tag.value);
config.tags = values.tags.map((tag: any) => tag.value);
} else {
config.tags = [];
}
@@ -107,20 +127,17 @@ const ClientsTable = ({
}
};
const getOptionsWithLabels = (options) => (
options.map((option) => ({
const getOptionsWithLabels = (options: any) =>
options.map((option: any) => ({
value: option,
label: option,
}))
);
}));
const getClient = (name, clients) => {
const client = clients.find((item) => name === item.name);
const getClient = (name: any, clients: any) => {
const client = clients.find((item: any) => name === item.name);
if (client) {
const {
upstreams, tags, whois_info, ...values
} = client;
const { upstreams, tags, ...values } = client;
return {
upstreams: (upstreams && upstreams.join('\n')) || '',
tags: (tags && getOptionsWithLabels(tags)) || [],
@@ -136,11 +153,11 @@ const ClientsTable = ({
blocked_services_schedule: {
time_zone: LOCAL_TIMEZONE_VALUE,
},
safe_search: { ...(safesearch || {}) },
safe_search: { ...(globalSettings?.safesearch || {}) },
};
};
const handleDelete = (data) => {
const handleDelete = (data: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(t('client_confirm_delete', { key: data.name }))) {
deleteClient(data);
@@ -161,13 +178,13 @@ const ClientsTable = ({
Header: t('table_client'),
accessor: 'ids',
minWidth: 150,
Cell: (row) => {
Cell: (row: any) => {
const { value } = row;
return (
<div className="logs__row o-hidden">
<span className="logs__text">
{value.map((address) => (
{value.map((address: any) => (
<div key={address} title={address}>
{address}
</div>
@@ -188,12 +205,8 @@ const ClientsTable = ({
Header: t('settings'),
accessor: 'use_global_settings',
minWidth: 120,
Cell: ({ value }) => {
const title = value ? (
<Trans>settings_global</Trans>
) : (
<Trans>settings_custom</Trans>
);
Cell: ({ value }: any) => {
const title = value ? <Trans>settings_global</Trans> : <Trans>settings_custom</Trans>;
return (
<div className="logs__row o-hidden">
@@ -206,7 +219,7 @@ const ClientsTable = ({
Header: t('blocked_services'),
accessor: 'blocked_services',
minWidth: 180,
Cell: (row) => {
Cell: (row: any) => {
const { value, original } = row;
if (original.use_global_blocked_services) {
@@ -216,7 +229,7 @@ const ClientsTable = ({
if (value && services.allServices) {
return (
<div className="logs__row logs__row--icons">
{value.map((service) => {
{value.map((service: any) => {
const serviceInfo = getService(services.allServices, service);
if (serviceInfo?.icon_svg) {
@@ -238,23 +251,16 @@ const ClientsTable = ({
);
}
return (
<div className="logs__row logs__row--icons">
</div>
);
return <div className="logs__row logs__row--icons"></div>;
},
},
{
Header: t('upstreams'),
accessor: 'upstreams',
minWidth: 120,
Cell: ({ value }) => {
const title = value && value.length > 0 ? (
<Trans>settings_custom</Trans>
) : (
<Trans>settings_global</Trans>
);
Cell: ({ value }: any) => {
const title =
value && value.length > 0 ? <Trans>settings_custom</Trans> : <Trans>settings_global</Trans>;
return (
<div className="logs__row o-hidden">
@@ -267,7 +273,7 @@ const ClientsTable = ({
Header: t('tags_title'),
accessor: 'tags',
minWidth: 140,
Cell: (row) => {
Cell: (row: any) => {
const { value } = row;
if (!value || value.length < 1) {
@@ -277,7 +283,7 @@ const ClientsTable = ({
return (
<div className="logs__row o-hidden">
<span className="logs__text">
{value.map((tag) => (
{value.map((tag: any) => (
<div key={tag} title={tag} className="logs__tag small">
{tag}
</div>
@@ -290,13 +296,10 @@ const ClientsTable = ({
{
Header: t('requests_count'),
id: 'statistics',
accessor: (row) => countClientsStatistics(
row.ids,
normalizedTopClients.auto,
),
sortMethod: (a, b) => b - a,
accessor: (row: any) => countClientsStatistics(row.ids, normalizedTopClients.auto),
sortMethod: (a: any, b: any) => b - a,
minWidth: 120,
Cell: (row) => {
Cell: (row: any) => {
const content = CellWrap(row);
if (!row.value) {
@@ -312,7 +315,7 @@ const ClientsTable = ({
maxWidth: 100,
sortable: false,
resizable: false,
Cell: (row) => {
Cell: (row: any) => {
const clientName = row.original.name;
return (
@@ -320,25 +323,25 @@ const ClientsTable = ({
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() => toggleClientModal({
type: MODAL_TYPE.EDIT_CLIENT,
name: clientName,
})
onClick={() =>
toggleClientModal({
type: MODAL_TYPE.EDIT_CLIENT,
name: clientName,
})
}
disabled={processingUpdating}
title={t('edit_table_action')}
>
title={t('edit_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => handleDelete({ name: clientName })}
disabled={processingDeleting}
title={t('delete_table_action')}
>
title={t('delete_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#delete" />
</svg>
@@ -353,11 +356,7 @@ const ClientsTable = ({
const tagsOptions = getOptionsWithLabels(supportedTags);
return (
<Card
title={t('clients_title')}
subtitle={t('clients_desc')}
bodyType="card-body box-body--settings"
>
<Card title={t('clients_title')} subtitle={t('clients_desc')} bodyType="card-body box-body--settings">
<>
<ReactTable
data={clients || []}
@@ -371,9 +370,9 @@ const ClientsTable = ({
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || 10}
onPageSizeChange={(size) => (
onPageSizeChange={(size: any) =>
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, size)
)}
}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
@@ -383,14 +382,15 @@ const ClientsTable = ({
loadingText={t('loading_table_status')}
noDataText={t('clients_not_found')}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD_FILTERS)}
disabled={processingAdding}
>
disabled={processingAdding}>
<Trans>client_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
@@ -407,21 +407,4 @@ const ClientsTable = ({
);
};
ClientsTable.propTypes = {
clients: PropTypes.array.isRequired,
normalizedTopClients: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired,
updateClient: PropTypes.func.isRequired,
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
modalClientName: PropTypes.string.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingDeleting: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
getStats: PropTypes.func.isRequired,
supportedTags: PropTypes.array.isRequired,
};
export default ClientsTable;

View File

@@ -1,491 +0,0 @@
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 { ScheduleForm } from '../../Filters/Services/ScheduleForm';
import {
toggleAllServices,
trimLinesAndRemoveEmpty,
captitalizeWords,
} from '../../../helpers/helpers';
import {
toNumber,
renderInputField,
renderGroupField,
CheckboxField,
renderServiceField,
renderTextareaField,
} from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME, UINT32_RANGE } 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 logAndStatsCheckboxes = [
{
name: 'ignore_querylog',
placeholder: 'ignore_query_log',
},
{
name: 'ignore_statistics',
placeholder: 'ignore_statistics',
},
];
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,
submitting,
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
handleClose,
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 handleScheduleSubmit = (values) => {
change('blocked_services_schedule', { ...values });
};
const tabs = {
settings: {
title: 'settings',
component: <div label="settings" title={props.t('main_settings')}>
<div className="form__label--bot form__label--bold">
{t('protection_section_label')}
</div>
{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 className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
{logAndStatsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
/>
</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>,
},
schedule_services: {
title: 'schedule_services',
component: (
<>
<div className="form__desc mb-4">
<Trans>schedule_services_desc_client</Trans>
</div>
<ScheduleForm
schedule={blockedServicesSchedule}
onScheduleSubmit={handleScheduleSubmit}
clientForm
/>
</>
),
},
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 className="form__label--bold mt-5 mb-3">
{t('upstream_dns_cache_configuration')}
</div>
<div className="form__group mb-2">
<Field
name="upstreams_cache_enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enable_upstream_dns_cache')}
/>
</div>
<div className="form__group form__group--settings">
<label
htmlFor="upstreams_cache_size"
className="form__label"
>
{t('dns_cache_size')}
</label>
<Field
name="upstreams_cache_size"
type="number"
component={renderInputField}
placeholder={t('enter_cache_size')}
className="form-control"
normalize={toNumber}
min={0}
max={UINT32_RANGE.MAX}
/>
</div>
</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://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax_ctag&from=ui&app=home"
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();
handleClose();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| 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,
handleClose: PropTypes.func.isRequired,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
blockedServicesSchedule: PropTypes.object,
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');
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
return {
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.CLIENT,
enableReinitialize: true,
validate,
}),
])(Form);

View File

@@ -0,0 +1,514 @@
import React, { useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { Field, FieldArray, reduxForm, formValueSelector, FormErrors } 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 { ScheduleForm } from '../../Filters/Services/ScheduleForm';
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
import {
toNumber,
renderInputField,
renderGroupField,
CheckboxField,
renderServiceField,
renderTextareaField,
} from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import './Service.css';
import { RootState } from '../../../initialState';
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 logAndStatsCheckboxes = [
{
name: 'ignore_querylog',
placeholder: 'ignore_query_log',
},
{
name: 'ignore_statistics',
placeholder: 'ignore_statistics',
},
];
const validate = (values: any): FormErrors<any, string> => {
const errors: {
name?: string;
ids?: string[];
} = {};
const { name, ids } = values;
errors.name = validateRequiredValue(name);
if (ids && ids.length) {
const idArrayErrors: any = [];
ids.forEach((id: any, idx: any) => {
idArrayErrors[idx] = validateRequiredValue(id) || validateClientId(id);
});
if (idArrayErrors.length) {
errors.ids = idArrayErrors;
}
}
// @ts-expect-error FIXME: ts migration
return errors;
};
const renderFieldsWrapper = (placeholder: any, buttonTitle: any) =>
function cell(row: any) {
const { fields } = row;
return (
<div className="form__group">
{fields.map((ip: any, index: any) => (
<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: any) => 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'));
interface renderMultiselectProps {
input: {
name: string;
value: string;
checked: boolean;
onChange: (...args: unknown[]) => unknown;
onBlur: (...args: unknown[]) => unknown;
};
placeholder?: string;
options?: unknown[];
}
const renderMultiselect = (props: renderMultiselectProps) => {
const { input, placeholder, options } = props;
return (
<Select
{...input}
options={options}
className="basic-multi-select"
classNamePrefix="select"
onChange={(value: any) => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
placeholder={placeholder}
blurInputOnSelect={false}
isMulti
/>
);
};
interface FormProps {
pristine: boolean;
handleSubmit: (...args: unknown[]) => string;
reset: (...args: unknown[]) => string;
change: (...args: unknown[]) => unknown;
submitting: boolean;
handleClose: (...args: unknown[]) => unknown;
useGlobalSettings?: boolean;
useGlobalServices?: boolean;
blockedServicesSchedule?: {
time_zone: string;
};
t: (...args: unknown[]) => string;
processingAdding: boolean;
processingUpdating: boolean;
invalid: boolean;
tagsOptions: unknown[];
initialValues?: {
safe_search: any;
};
}
let Form = (props: FormProps) => {
const {
t,
handleSubmit,
reset,
change,
submitting,
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
handleClose,
processingAdding,
processingUpdating,
invalid,
tagsOptions,
initialValues,
} = props;
const services = useSelector((store: RootState) => store?.services);
const { safe_search } = initialValues;
const safeSearchServices = { ...safe_search };
delete safeSearchServices.enabled;
const [activeTabLabel, setActiveTabLabel] = useState('settings');
const handleScheduleSubmit = (values: any) => {
change('blocked_services_schedule', { ...values });
};
const tabs = {
settings: {
title: 'settings',
component: (
<div title={props.t('main_settings')}>
<div className="form__label--bot form__label--bold">{t('protection_section_label')}</div>
{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 className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
{logAndStatsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
/>
</div>
))}
</div>
),
},
block_services: {
title: 'block_services',
component: (
<div 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: any) => (
<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>
),
},
schedule_services: {
title: 'schedule_services',
component: (
<>
<div className="form__desc mb-4">
<Trans>schedule_services_desc_client</Trans>
</div>
<ScheduleForm
schedule={blockedServicesSchedule}
onScheduleSubmit={handleScheduleSubmit}
clientForm
/>
</>
),
},
upstream_dns: {
title: 'upstream_dns',
component: (
<div 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 className="form__label--bold mt-5 mb-3">{t('upstream_dns_cache_configuration')}</div>
<div className="form__group mb-2">
<Field
name="upstreams_cache_enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enable_upstream_dns_cache')}
/>
</div>
<div className="form__group form__group--settings">
<label htmlFor="upstreams_cache_size" className="form__label">
{t('dns_cache_size')}
</label>
<Field
name="upstreams_cache_size"
type="number"
component={renderInputField}
placeholder={t('enter_cache_size')}
className="form-control"
normalize={toNumber}
min={0}
max={UINT32_RANGE.MAX}
/>
</div>
</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: any) => 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://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax_ctag&from=ui&app=home"
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();
handleClose();
}}>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingAdding || processingUpdating}>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
const selector = formValueSelector(FORM_NAME.CLIENT);
Form = connect((state) => {
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
return {
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.CLIENT,
enableReinitialize: true,
validate,
}),
])(Form);

View File

@@ -1,19 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { MODAL_TYPE } from '../../../helpers/constants';
import Form from './Form';
const getInitialData = ({
initial, modalType, clientId, clientName,
}) => {
const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
if (initial && initial.blocked_services) {
const { blocked_services } = initial;
const blocked = {};
blocked_services.forEach((service) => {
blocked_services.forEach((service: any) => {
blocked[service] = true;
});
@@ -34,6 +33,19 @@ const getInitialData = ({
return initial;
};
interface ModalProps {
isModalOpen: boolean;
modalType: string;
currentClientData: object;
handleSubmit: (values: any) => void;
handleClose: (...args: unknown[]) => unknown;
processingAdding: boolean;
processingUpdating: boolean;
tagsOptions: unknown[];
t: (...args: unknown[]) => string;
clientId?: string;
}
const Modal = ({
isModalOpen,
modalType,
@@ -45,7 +57,7 @@ const Modal = ({
tagsOptions,
clientId,
t,
}) => {
}: ModalProps) => {
const initialData = getInitialData({
initial: currentClientData,
modalType,
@@ -58,21 +70,18 @@ const Modal = ({
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={handleClose}
>
onRequestClose={handleClose}>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{modalType === MODAL_TYPE.EDIT_CLIENT ? (
<Trans>client_edit</Trans>
) : (
<Trans>client_new</Trans>
)}
{modalType === MODAL_TYPE.EDIT_CLIENT ? <Trans>client_edit</Trans> : <Trans>client_new</Trans>}
</h4>
<button type="button" className="close" onClick={handleClose}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{ ...initialData }}
onSubmit={handleSubmit}
@@ -86,17 +95,4 @@ const Modal = ({
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
currentClientData: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
handleClose: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired,
t: PropTypes.func.isRequired,
clientId: PropTypes.string,
};
export default withTranslation()(Modal);

View File

@@ -24,9 +24,9 @@
.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));
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 {

View File

@@ -1,34 +1,60 @@
import React, { Component, Fragment } from 'react';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { ClientsTable } from './ClientsTable';
import AutoClients from './AutoClients';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Clients extends Component {
import AutoClients from './AutoClients';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import { ClientsData, DashboardData, StatsData } from '../../../initialState';
interface ClientsProps {
t: (...args: unknown[]) => string;
dashboard: DashboardData;
stats: StatsData;
clients: ClientsData;
toggleClientModal: (...args: unknown[]) => unknown;
deleteClient: (...args: unknown[]) => string;
addClient: (...args: unknown[]) => string;
updateClient: (...args: unknown[]) => string;
getClients: (...args: unknown[]) => unknown;
getStats: (...args: unknown[]) => unknown;
}
class Clients extends Component<ClientsProps> {
componentDidMount() {
this.props.getClients();
this.props.getStats();
}
render() {
const {
t,
dashboard,
stats,
clients,
addClient,
updateClient,
deleteClient,
toggleClientModal,
getStats,
} = this.props;
return (
<Fragment>
<PageTitle title={t('client_settings')} />
{(stats.processingStats || dashboard.processingClients) && <Loading />}
{!stats.processingStats && !dashboard.processingClients && (
<Fragment>
@@ -48,6 +74,7 @@ class Clients extends Component {
getStats={getStats}
supportedTags={dashboard.supportedTags}
/>
<AutoClients
autoClients={dashboard.autoClients}
normalizedTopClients={stats.normalizedTopClients}
@@ -59,17 +86,4 @@ class Clients extends Component {
}
}
Clients.propTypes = {
t: PropTypes.func.isRequired,
dashboard: PropTypes.object.isRequired,
stats: PropTypes.object.isRequired,
clients: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired,
updateClient: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
getStats: PropTypes.func.isRequired,
};
export default withTranslation()(Clients);

View File

@@ -3,7 +3,7 @@ import React, { Fragment } from 'react';
import { normalizeWhois } from '../../../helpers/helpers';
import { WHOIS_ICONS } from '../../../helpers/constants';
const getFormattedWhois = (value, t) => {
const getFormattedWhois = (value: any, t: any) => {
const whoisInfo = normalizeWhois(value);
const whoisKeys = Object.keys(whoisInfo);
@@ -29,12 +29,15 @@ const getFormattedWhois = (value, t) => {
return '';
};
const whoisCell = (t) => function cell(row) {
const { value } = row;
const whoisCell = (t: any) =>
function cell(row: any) {
const { value } = row;
return <div className="logs__row o-hidden">
<div className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</div>
</div>;
};
return (
<div className="logs__row o-hidden">
<div className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</div>
</div>
);
};
export default whoisCell;

View File

@@ -1,162 +0,0 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import {
renderInputField,
toNumber,
} from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateNotInRange,
} from '../../../helpers/validators';
const FormDHCPv4 = ({
handleSubmit,
submitting,
processingConfig,
ipv4placeholders,
}) => {
const { t } = useTranslation();
const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const interfaces = useSelector((state) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv4 = useSelector(
(state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {})
.some(Boolean);
const invalid = dhcp?.syncErrors || interfaces?.syncErrors || !isInterfaceIncludesIpv4
|| isEmptyConfig || submitting || processingConfig;
const validateRequired = useCallback((value) => {
if (isEmptyConfig) {
return undefined;
}
return validateRequiredValue(value);
}, [isEmptyConfig]);
return <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="v4.gateway_ip"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.gateway_ip)}
validate={[
validateIpv4,
validateRequired,
validateNotInRange,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="v4.subnet_mask"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.subnet_mask)}
validate={[
validateRequired,
validateGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v4.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_start)}
validate={[
validateIpv4,
validateIpForGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="col">
<Field
name="v4.range_end"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_end)}
validate={[
validateIpv4,
validateIpv4RangeEnd,
validateIpForGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="v4.lease_duration"
component={renderInputField}
type="number"
className="form-control"
placeholder={t(ipv4placeholders.lease_duration)}
validate={validateRequired}
normalize={toNumber}
min={1}
max={UINT32_RANGE.MAX}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
</div>
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={invalid}
>
{t('save_config')}
</button>
</div>
</form>;
};
FormDHCPv4.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
change: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
ipv4placeholders: PropTypes.object.isRequired,
};
export default reduxForm({
form: FORM_NAME.DHCPv4,
})(FormDHCPv4);

View File

@@ -0,0 +1,166 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import { renderInputField, toNumber } from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateNotInRange,
} from '../../../helpers/validators';
import { RootState } from '../../../initialState';
interface FormDHCPv4Props {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
initialValues: { v4?: any };
processingConfig?: boolean;
change: (field: string, value: any) => void;
reset: () => void;
ipv4placeholders?: {
gateway_ip: string;
subnet_mask: string;
range_start: string;
range_end: string;
lease_duration: string;
};
}
const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholders }: FormDHCPv4Props) => {
const { t } = useTranslation();
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const invalid =
dhcp?.syncErrors ||
interfaces?.syncErrors ||
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
submitting ||
processingConfig;
const validateRequired = useCallback(
(value) => {
if (isEmptyConfig) {
return undefined;
}
return validateRequiredValue(value);
},
[isEmptyConfig],
);
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="v4.gateway_ip"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.gateway_ip)}
validate={[validateIpv4, validateRequired, validateNotInRange]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="v4.subnet_mask"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.subnet_mask)}
validate={[validateRequired, validateGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v4.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_start)}
validate={[validateIpv4, validateIpForGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="col">
<Field
name="v4.range_end"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_end)}
validate={[validateIpv4, validateIpv4RangeEnd, validateIpForGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="v4.lease_duration"
component={renderInputField}
type="number"
className="form-control"
placeholder={t(ipv4placeholders.lease_duration)}
validate={validateRequired}
normalize={toNumber}
min={1}
max={UINT32_RANGE.MAX}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
{t('save_config')}
</button>
</div>
</form>
);
};
export default reduxForm<
Record<string, any>,
Omit<FormDHCPv4Props, 'submitting' | 'handleSubmit' | 'reset' | 'change'>
>({
form: FORM_NAME.DHCPv4,
})(FormDHCPv4);

View File

@@ -1,117 +0,0 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import {
renderInputField,
toNumber,
} from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
const FormDHCPv6 = ({
handleSubmit,
submitting,
processingConfig,
ipv6placeholders,
}) => {
const { t } = useTranslation();
const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv6], shallowEqual);
const interfaces = useSelector((state) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv6 = useSelector(
(state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses,
);
const isEmptyConfig = !Object.values(dhcp?.values?.v6 ?? {})
.some(Boolean);
const invalid = dhcp?.syncErrors || interfaces?.syncErrors || !isInterfaceIncludesIpv6
|| isEmptyConfig || submitting || processingConfig;
const validateRequired = useCallback((value) => {
if (isEmptyConfig) {
return undefined;
}
return validateRequiredValue(value);
}, [isEmptyConfig]);
return <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v6.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv6placeholders.range_start)}
validate={[validateIpv6, validateRequired]}
disabled={!isInterfaceIncludesIpv6}
/>
</div>
<div className="col">
<Field
name="v6.range_end"
component="input"
type="text"
className="form-control disabled cursor--not-allowed"
placeholder={t(ipv6placeholders.range_end)}
value={t(ipv6placeholders.range_end)}
disabled
/>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6 form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="v6.lease_duration"
component={renderInputField}
type="number"
className="form-control"
placeholder={t(ipv6placeholders.lease_duration)}
validate={validateRequired}
normalizeOnBlur={toNumber}
min={1}
max={UINT32_RANGE.MAX}
disabled={!isInterfaceIncludesIpv6}
/>
</div>
</div>
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={invalid}
>
{t('save_config')}
</button>
</div>
</form>;
};
FormDHCPv6.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
change: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
ipv6placeholders: PropTypes.object.isRequired,
};
export default reduxForm({
form: FORM_NAME.DHCPv6,
})(FormDHCPv6);

View File

@@ -0,0 +1,131 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import { renderInputField, toNumber } from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
import { RootState } from '../../../initialState';
interface FormDHCPv6Props {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
initialValues: {
v6?: any;
};
change: (field: string, value: any) => void;
reset: () => void;
processingConfig?: boolean;
ipv6placeholders?: {
range_start: string;
range_end: string;
lease_duration: string;
};
}
const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholders }: FormDHCPv6Props) => {
const { t } = useTranslation();
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv6], shallowEqual);
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv6 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses,
);
const isEmptyConfig = !Object.values(dhcp?.values?.v6 ?? {}).some(Boolean);
const invalid =
dhcp?.syncErrors ||
interfaces?.syncErrors ||
!isInterfaceIncludesIpv6 ||
isEmptyConfig ||
submitting ||
processingConfig;
const validateRequired = useCallback(
(value) => {
if (isEmptyConfig) {
return undefined;
}
return validateRequiredValue(value);
},
[isEmptyConfig],
);
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v6.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv6placeholders.range_start)}
validate={[validateIpv6, validateRequired]}
disabled={!isInterfaceIncludesIpv6}
/>
</div>
<div className="col">
<Field
name="v6.range_end"
component="input"
type="text"
className="form-control disabled cursor--not-allowed"
placeholder={t(ipv6placeholders.range_end)}
value={t(ipv6placeholders.range_end)}
disabled
/>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6 form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="v6.lease_duration"
component={renderInputField}
type="number"
className="form-control"
placeholder={t(ipv6placeholders.lease_duration)}
validate={validateRequired}
normalizeOnBlur={toNumber}
min={1}
max={UINT32_RANGE.MAX}
disabled={!isInterfaceIncludesIpv6}
/>
</div>
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
{t('save_config')}
</button>
</div>
</form>
);
};
export default reduxForm<
Record<string, any>,
Omit<FormDHCPv6Props, 'handleSubmit' | 'change' | 'submitting' | 'reset'>
>({
form: FORM_NAME.DHCPv6,
})(FormDHCPv6);

View File

@@ -1,108 +0,0 @@
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import propTypes from 'prop-types';
import { renderSelectField } from '../../../helpers/form';
import { validateRequiredValue } from '../../../helpers/validators';
import { FORM_NAME } from '../../../helpers/constants';
const renderInterfaces = (interfaces) => Object.keys(interfaces)
.map((item) => {
const option = interfaces[item];
const { name } = option;
const [interfaceIPv4] = option?.ipv4_addresses ?? [];
const [interfaceIPv6] = option?.ipv6_addresses ?? [];
const optionContent = [name, interfaceIPv4, interfaceIPv6].filter(Boolean).join(' - ');
return <option value={name} key={name}>{optionContent}</option>;
});
const getInterfaceValues = ({
gateway_ip,
hardware_address,
ip_addresses,
}) => [
{
name: 'dhcp_form_gateway_input',
value: gateway_ip,
},
{
name: 'dhcp_hardware_address',
value: hardware_address,
},
{
name: 'dhcp_ip_addresses',
value: ip_addresses,
render: (ip_addresses) => ip_addresses
.map((ip) => <span key={ip} className="interface__ip">{ip}</span>),
},
];
const renderInterfaceValues = ({
gateway_ip,
hardware_address,
ip_addresses,
}) => <div className='d-flex align-items-end dhcp__interfaces-info'>
<ul className="list-unstyled m-0">
{getInterfaceValues({
gateway_ip,
hardware_address,
ip_addresses,
}).map(({ name, value, render }) => value && <li key={name}>
<span className="interface__title"><Trans>{name}</Trans>: </span>
{render?.(value) || value}
</li>)}
</ul>
</div>;
const Interfaces = () => {
const { t } = useTranslation();
const {
processingInterfaces,
interfaces,
enabled,
} = useSelector((store) => store.dhcp, shallowEqual);
const interface_name = useSelector(
(store) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name,
);
if (processingInterfaces || !interfaces) {
return null;
}
const interfaceValue = interface_name && interfaces[interface_name];
return <div className="row dhcp__interfaces">
<div className="col col__dhcp">
<Field
name="interface_name"
component={renderSelectField}
className="form-control custom-select pl-4 col-md"
validate={[validateRequiredValue]}
label='dhcp_interface_select'
>
<option value='' disabled={enabled}>
{t('dhcp_interface_select')}
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
{interfaceValue
&& renderInterfaceValues(interfaceValue)}
</div>;
};
renderInterfaceValues.propTypes = {
gateway_ip: propTypes.string.isRequired,
hardware_address: propTypes.string.isRequired,
ip_addresses: propTypes.arrayOf(propTypes.string).isRequired,
};
export default reduxForm({
form: FORM_NAME.DHCP_INTERFACES,
})(Interfaces);

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import { renderSelectField } from '../../../helpers/form';
import { validateRequiredValue } from '../../../helpers/validators';
import { FORM_NAME } from '../../../helpers/constants';
import { RootState } from '../../../initialState';
const renderInterfaces = (interfaces: any) =>
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
const [interfaceIPv4] = option?.ipv4_addresses ?? [];
const [interfaceIPv6] = option?.ipv6_addresses ?? [];
const optionContent = [name, interfaceIPv4, interfaceIPv6].filter(Boolean).join(' - ');
return (
<option value={name} key={name}>
{optionContent}
</option>
);
});
const getInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: any) => [
{
name: 'dhcp_form_gateway_input',
value: gateway_ip,
},
{
name: 'dhcp_hardware_address',
value: hardware_address,
},
{
name: 'dhcp_ip_addresses',
value: ip_addresses,
render: (ip_addresses: any) =>
ip_addresses.map((ip: any) => (
<span key={ip} className="interface__ip">
{ip}
</span>
)),
},
];
interface renderInterfaceValuesProps {
gateway_ip: string;
hardware_address: string;
ip_addresses: string[];
}
const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: renderInterfaceValuesProps) => (
<div className="d-flex align-items-end dhcp__interfaces-info">
<ul className="list-unstyled m-0">
{getInterfaceValues({
gateway_ip,
hardware_address,
ip_addresses,
}).map(
({ name, value, render }) =>
value && (
<li key={name}>
<span className="interface__title">
<Trans>{name}</Trans>:{' '}
</span>
{render?.(value) || value}
</li>
),
)}
</ul>
</div>
);
const Interfaces = () => {
const { t } = useTranslation();
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp, shallowEqual);
const interface_name =
useSelector((store: RootState) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
if (processingInterfaces || !interfaces) {
return null;
}
const interfaceValue = interface_name && interfaces[interface_name];
return (
<div className="row dhcp__interfaces">
<div className="col col__dhcp">
<Field
name="interface_name"
component={renderSelectField}
className="form-control custom-select pl-4 col-md"
validate={[validateRequiredValue]}
label="dhcp_interface_select">
<option value="" disabled={enabled}>
{t('dhcp_interface_select')}
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
{interfaceValue && renderInterfaceValues({
gateway_ip: interfaceValue.gateway_ip,
hardware_address: interfaceValue.hardware_address,
ip_addresses: interfaceValue.ip_addresses
})}
</div>
);
};
export default reduxForm({
form: FORM_NAME.DHCP_INTERFACES,
})(Interfaces);

View File

@@ -1,14 +1,24 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import { Trans, withTranslation } from 'react-i18next';
import { LEASES_TABLE_DEFAULT_PAGE_SIZE, MODAL_TYPE } from '../../../helpers/constants';
import { sortIp } from '../../../helpers/helpers';
import { toggleLeaseModal } from '../../../actions';
class Leases extends Component {
cellWrap = ({ value }) => (
interface LeasesProps {
leases?: unknown[];
t?: (...args: unknown[]) => string;
dispatch?: (...args: unknown[]) => unknown;
disabledLeasesButton?: boolean;
}
class Leases extends Component<LeasesProps> {
cellWrap = ({ value }: any) => (
<div className="logs__row o-hidden">
<span className="logs__text" title={value}>
{value}
@@ -16,15 +26,17 @@ class Leases extends Component {
</div>
);
convertToStatic = (data) => () => {
convertToStatic = (data: any) => () => {
const { dispatch } = this.props;
dispatch(toggleLeaseModal({
type: MODAL_TYPE.ADD_LEASE,
config: data,
}));
}
dispatch(
toggleLeaseModal({
type: MODAL_TYPE.ADD_LEASE,
config: data,
}),
);
};
makeStatic = ({ row }) => {
makeStatic = ({ row }: any) => {
const { t, disabledLeasesButton } = this.props;
return (
<div className="logs__row logs__row--center">
@@ -33,15 +45,14 @@ class Leases extends Component {
className="btn btn-icon btn-icon--green btn-outline-success btn-sm"
title={t('make_static')}
onClick={this.convertToStatic(row)}
disabled={disabledLeasesButton}
>
disabled={disabledLeasesButton}>
<svg className="icons icon12">
<use xlinkHref="#plus" />
</svg>
</button>
</div>
);
}
};
render() {
const { leases, t } = this.props;
@@ -54,23 +65,27 @@ class Leases extends Component {
accessor: 'mac',
minWidth: 180,
Cell: this.cellWrap,
}, {
},
{
Header: 'IP',
accessor: 'ip',
minWidth: 230,
Cell: this.cellWrap,
sortMethod: sortIp,
}, {
},
{
Header: <Trans>dhcp_table_hostname</Trans>,
accessor: 'hostname',
minWidth: 230,
Cell: this.cellWrap,
}, {
},
{
Header: <Trans>dhcp_table_expires</Trans>,
accessor: 'expires',
minWidth: 220,
Cell: this.cellWrap,
}, {
},
{
Header: <Trans>actions_table_header</Trans>,
Cell: this.makeStatic,
},
@@ -86,11 +101,9 @@ class Leases extends Component {
}
}
Leases.propTypes = {
leases: PropTypes.array,
t: PropTypes.func,
dispatch: PropTypes.func,
disabledLeasesButton: PropTypes.bool,
};
export default withTranslation()(connect(() => ({}), (dispatch) => ({ dispatch }))(Leases));
export default withTranslation()(
connect(
() => ({}),
(dispatch) => ({ dispatch }),
)(Leases),
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
@@ -13,20 +13,32 @@ import {
validateIpGateway,
} from '../../../../helpers/validators';
import { FORM_NAME } from '../../../../helpers/constants';
import { toggleLeaseModal } from '../../../../actions';
const Form = ({
handleSubmit,
reset,
pristine,
submitting,
processingAdding,
cidr,
isEdit,
}) => {
import { toggleLeaseModal } from '../../../../actions';
import { RootState } from '../../../../initialState';
interface FormStaticLeaseProps {
initialValues?: {
mac?: string;
ip?: string;
hostname?: string;
cidr?: string;
gatewayIp?: string;
};
pristine: boolean;
handleSubmit: (...args: unknown[]) => string;
reset: () => void;
submitting: boolean;
processingAdding?: boolean;
cidr?: string;
isEdit?: boolean;
}
const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cidr, isEdit }: FormStaticLeaseProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const dynamicLease = useSelector((store) => store.dhcp.leaseModalConfig, shallowEqual);
const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);
const onClick = () => {
reset();
@@ -49,6 +61,7 @@ const Form = ({
disabled={isEdit}
/>
</div>
<div className="form__group">
<Field
id="ip"
@@ -57,14 +70,10 @@ const Form = ({
type="text"
className="form-control"
placeholder={t('form_enter_subnet_ip', { cidr })}
validate={[
validateRequiredValue,
validateIpv4,
validateIpv4InCidr,
validateIpGateway,
]}
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr, validateIpGateway]}
/>
</div>
<div className="form__group">
<Field
id="hostname"
@@ -83,15 +92,14 @@ const Form = ({
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={onClick}
>
onClick={onClick}>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || processingAdding || (pristine && !dynamicLease)}
>
disabled={submitting || processingAdding || (pristine && !dynamicLease)}>
<Trans>save_btn</Trans>
</button>
</div>
@@ -100,21 +108,7 @@ const Form = ({
);
};
Form.propTypes = {
initialValues: PropTypes.shape({
mac: PropTypes.string.isRequired,
ip: PropTypes.string.isRequired,
hostname: PropTypes.string.isRequired,
cidr: PropTypes.string.isRequired,
gatewayIp: PropTypes.string,
}),
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
processingAdding: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
isEdit: PropTypes.bool,
};
export default reduxForm({ form: FORM_NAME.LEASE })(Form);
export default reduxForm<
Record<string, any>,
Omit<FormStaticLeaseProps, 'submitting' | 'handleSubmit' | 'reset' | 'pristine'>
>({ form: FORM_NAME.LEASE })(Form);

View File

@@ -1,11 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import { toggleLeaseModal } from '../../../../actions';
import { MODAL_TYPE } from '../../../../helpers/constants';
import { RootState } from '../../../../initialState';
interface ModalProps {
isModalOpen: boolean;
modalType: string;
handleSubmit: (values: any) => void;
processingAdding: boolean;
cidr: string;
gatewayIp?: string;
}
const Modal = ({
isModalOpen,
@@ -13,24 +25,20 @@ const Modal = ({
handleSubmit,
processingAdding,
cidr,
rangeStart,
rangeEnd,
gatewayIp,
}) => {
}: ModalProps) => {
const dispatch = useDispatch();
const toggleModal = () => dispatch(toggleLeaseModal());
const leaseInitialData = useSelector(
(state) => state.dhcp.leaseModalConfig, shallowEqual,
) || {};
const leaseInitialData = useSelector((state: RootState) => state.dhcp.leaseModalConfig, shallowEqual);
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={toggleModal}
>
onRequestClose={toggleModal}>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
@@ -40,25 +48,23 @@ const Modal = ({
<Trans>dhcp_new_static_lease</Trans>
)}
</h4>
<button type="button" className="close" onClick={toggleModal}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{
mac: leaseInitialData.mac ?? '',
ip: leaseInitialData.ip ?? '',
hostname: leaseInitialData.hostname ?? '',
mac: leaseInitialData?.mac ?? '',
ip: leaseInitialData?.ip ?? '',
hostname: leaseInitialData?.hostname ?? '',
cidr,
rangeStart,
rangeEnd,
gatewayIp,
}}
onSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
isEdit={modalType === MODAL_TYPE.EDIT_LEASE}
/>
</div>
@@ -66,15 +72,4 @@ const Modal = ({
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
handleSubmit: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
rangeStart: PropTypes.string,
rangeEnd: PropTypes.string,
gatewayIp: PropTypes.string,
};
export default withTranslation()(Modal);

View File

@@ -1,26 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { LEASES_TABLE_DEFAULT_PAGE_SIZE, MODAL_TYPE } from '../../../../helpers/constants';
import { sortIp } from '../../../../helpers/helpers';
import Modal from './Modal';
import {
addStaticLease,
removeStaticLease,
toggleLeaseModal,
updateStaticLease,
} from '../../../../actions';
const cellWrap = ({ value }) => (
import { sortIp } from '../../../../helpers/helpers';
import Modal from './Modal';
import { addStaticLease, removeStaticLease, toggleLeaseModal, updateStaticLease } from '../../../../actions';
interface cellWrapProps {
value: string;
}
const cellWrap = ({ value }: cellWrapProps) => (
<div className="logs__row o-hidden">
<span className="logs__text" title={value}>
{value}
</span>
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
interface StaticLeasesProps {
staticLeases: unknown[];
isModalOpen: boolean;
modalType: string;
processingAdding: boolean;
processingDeleting: boolean;
processingUpdating: boolean;
cidr: string;
gatewayIp?: string;
}
const StaticLeases = ({
isModalOpen,
modalType,
@@ -29,14 +42,12 @@ const StaticLeases = ({
processingUpdating,
staticLeases,
cidr,
rangeStart,
rangeEnd,
gatewayIp,
}) => {
}: StaticLeasesProps) => {
const [t] = useTranslation();
const dispatch = useDispatch();
const handleSubmit = (data) => {
const handleSubmit = (data: any) => {
const { mac, ip, hostname } = data;
if (modalType === MODAL_TYPE.EDIT_LEASE) {
@@ -46,15 +57,17 @@ const StaticLeases = ({
}
};
const handleDelete = (ip, mac, hostname = '') => {
const handleDelete = (ip: any, mac: any, hostname = '') => {
const name = hostname || ip;
// eslint-disable-next-line no-alert
if (window.confirm(t('delete_confirm', { key: name }))) {
dispatch(removeStaticLease({
ip,
mac,
hostname,
}));
dispatch(
removeStaticLease({
ip,
mac,
hostname,
}),
);
}
};
@@ -89,7 +102,7 @@ const StaticLeases = ({
sortable: false,
resizable: false,
// eslint-disable-next-line react/display-name
Cell: (row) => {
Cell: (row: any) => {
const { ip, mac, hostname } = row.original;
return (
@@ -97,24 +110,27 @@ const StaticLeases = ({
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() => dispatch(toggleLeaseModal({
type: MODAL_TYPE.EDIT_LEASE,
config: { ip, mac, hostname },
}))}
onClick={() =>
dispatch(
toggleLeaseModal({
type: MODAL_TYPE.EDIT_LEASE,
config: { ip, mac, hostname },
}),
)
}
disabled={processingUpdating}
title={t('edit_table_action')}
>
title={t('edit_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => handleDelete(ip, mac, hostname)}
disabled={processingDeleting}
title={t('delete_table_action')}
>
title={t('delete_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#delete" />
</svg>
@@ -131,35 +147,17 @@ const StaticLeases = ({
className="-striped -highlight card-table-overflow"
minRows={6}
/>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
handleSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
gatewayIp={gatewayIp}
/>
</>
);
};
StaticLeases.propTypes = {
staticLeases: PropTypes.array.isRequired,
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingDeleting: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
rangeStart: PropTypes.string,
rangeEnd: PropTypes.string,
gatewayIp: PropTypes.string,
};
cellWrap.propTypes = {
value: PropTypes.string.isRequired,
};
export default StaticLeases;

View File

@@ -1,312 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import { destroy } from 'redux-form';
import {
DHCP_DESCRIPTION_PLACEHOLDERS,
DHCP_FORM_NAMES,
STATUS_RESPONSE,
FORM_NAME,
} from '../../../helpers/constants';
import Leases from './Leases';
import StaticLeases from './StaticLeases/index';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import {
findActiveDhcp,
getDhcpInterfaces,
getDhcpStatus,
resetDhcp,
setDhcpConfig,
resetDhcpLeases,
toggleDhcp,
toggleLeaseModal,
} from '../../../actions';
import FormDHCPv4 from './FormDHCPv4';
import FormDHCPv6 from './FormDHCPv6';
import Interfaces from './Interfaces';
import {
calculateDhcpPlaceholdersIpv4,
calculateDhcpPlaceholdersIpv6,
subnetMaskToBitMask,
} from '../../../helpers/helpers';
import './index.css';
const Dhcp = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
processingStatus,
processingConfig,
processing,
processingInterfaces,
check,
leases,
staticLeases,
isModalOpen,
processingAdding,
processingDeleting,
processingUpdating,
processingDhcp,
v4,
v6,
interface_name: interfaceName,
enabled,
dhcp_available,
interfaces,
modalType,
} = useSelector((state) => state.dhcp, shallowEqual);
const interface_name = useSelector(
(state) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name,
);
const isInterfaceIncludesIpv4 = useSelector(
(state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
useEffect(() => {
dispatch(getDhcpStatus());
}, []);
useEffect(() => {
if (dhcp_available) {
dispatch(getDhcpInterfaces());
}
}, [dhcp_available]);
useEffect(() => {
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
const gateway_ip = interfaces?.[interface_name]?.gateway_ip;
const v4placeholders = ipv4
? calculateDhcpPlaceholdersIpv4(ipv4, gateway_ip)
: DHCP_DESCRIPTION_PLACEHOLDERS.ipv4;
const v6placeholders = ipv6
? calculateDhcpPlaceholdersIpv6()
: DHCP_DESCRIPTION_PLACEHOLDERS.ipv6;
setIpv4Placeholders(v4placeholders);
setIpv6Placeholders(v6placeholders);
}, [interface_name]);
const clear = () => {
// eslint-disable-next-line no-alert
if (window.confirm(t('dhcp_reset'))) {
Object.values(DHCP_FORM_NAMES)
.forEach((formName) => dispatch(destroy(formName)));
dispatch(resetDhcp());
dispatch(getDhcpStatus());
}
};
const handleSubmit = (values) => {
dispatch(setDhcpConfig({
interface_name,
...values,
}));
};
const handleReset = () => {
if (window.confirm(t('dhcp_reset_leases_confirm'))) {
dispatch(resetDhcpLeases());
}
};
const enteredSomeV4Value = Object.values(v4)
.some(Boolean);
const enteredSomeV6Value = Object.values(v6)
.some(Boolean);
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
const getToggleDhcpButton = () => {
const filledConfig = interface_name && (Object.values(v4)
.every(Boolean) || Object.values(v6)
.every(Boolean));
const className = classNames('btn btn-sm', {
'btn-gray': enabled,
'btn-outline-success': !enabled,
});
const onClickDisable = () => dispatch(toggleDhcp({ enabled }));
const onClickEnable = () => {
const values = {
enabled,
interface_name,
v4: enteredSomeV4Value ? v4 : {},
v6: enteredSomeV6Value ? v6 : {},
};
dispatch(toggleDhcp(values));
};
return <button
type="button"
className={className}
onClick={enabled ? onClickDisable : onClickEnable}
disabled={processingDhcp || processingConfig
|| (!enabled && (!filledConfig || !check))}
>
<Trans>{enabled ? 'dhcp_disable' : 'dhcp_enable'}</Trans>
</button>;
};
const statusButtonClass = classNames('btn btn-sm dhcp-form__button', {
'btn-loading btn-primary': processingStatus,
'btn-outline-primary': !processingStatus,
});
const onClick = () => dispatch(findActiveDhcp(interface_name));
const toggleModal = () => dispatch(toggleLeaseModal());
const initialV4 = enteredSomeV4Value ? v4 : {};
const initialV6 = enteredSomeV6Value ? v6 : {};
if (processing || processingInterfaces) {
return <Loading />;
}
if (!processing && !dhcp_available) {
return <div className="text-center pt-5">
<h2>
<Trans>unavailable_dhcp</Trans>
</h2>
<h4>
<Trans>unavailable_dhcp_desc</Trans>
</h4>
</div>;
}
const toggleDhcpButton = getToggleDhcpButton();
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const disabledLeasesButton = Boolean(dhcp?.syncErrors || interfaces?.syncErrors
|| !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values);
const cidr = inputtedIPv4values ? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}` : '';
return <>
<PageTitle title={t('dhcp_settings')} subtitle={t('dhcp_description')} containerClass="page-title--dhcp">
{toggleDhcpButton}
<button
type="button"
className={statusButtonClass}
onClick={onClick}
disabled={enabled || !interface_name || processingConfig}
>
<Trans>check_dhcp_servers</Trans>
</button>
<button
type="button"
className='btn btn-sm btn-outline-secondary'
disabled={!enteredSomeValue || processingConfig}
onClick={clear}
>
<Trans>reset_settings</Trans>
</button>
</PageTitle>
{!processing && !processingInterfaces
&& <>
{!enabled
&& check
&& (check.v4.other_server.found !== STATUS_RESPONSE.NO
|| check.v6.other_server.found !== STATUS_RESPONSE.NO)
&& <div className="mb-5">
<hr />
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
</div>}
<Interfaces
initialValues={{ interface_name: interfaceName }}
/>
<Card
title={t('dhcp_ipv4_settings')}
bodyType="card-body box-body--settings"
>
<div>
<FormDHCPv4
onSubmit={handleSubmit}
initialValues={{ v4: initialV4 }}
processingConfig={processingConfig}
ipv4placeholders={ipv4placeholders}
/>
</div>
</Card>
<Card
title={t('dhcp_ipv6_settings')}
bodyType="card-body box-body--settings"
>
<div>
<FormDHCPv6
onSubmit={handleSubmit}
initialValues={{ v6: initialV6 }}
processingConfig={processingConfig}
ipv6placeholders={ipv6placeholders}
/>
</div>
</Card>
{enabled
&& <Card
title={t('dhcp_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Leases leases={leases} disabledLeasesButton={disabledLeasesButton}/>
</div>
</div>
</Card>}
<Card
title={t('dhcp_static_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={staticLeases}
isModalOpen={isModalOpen}
toggleModal={toggleModal}
modalType={modalType}
processingAdding={processingAdding}
processingDeleting={processingDeleting}
processingUpdating={processingUpdating}
cidr={cidr}
rangeStart={dhcp?.values?.v4?.range_start}
rangeEnd={dhcp?.values?.v4?.range_end}
gatewayIp={dhcp?.values?.v4?.gateway_ip}
/>
<div className="btn-list mt-2">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={toggleModal}
disabled={disabledLeasesButton}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standard mt-3"
onClick={handleReset}
>
<Trans>dhcp_reset_leases</Trans>
</button>
</div>
</div>
</div>
</Card>
</>}
</>;
};
export default Dhcp;

View File

@@ -0,0 +1,321 @@
import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import { destroy } from 'redux-form';
import { DHCP_DESCRIPTION_PLACEHOLDERS, DHCP_FORM_NAMES, STATUS_RESPONSE, FORM_NAME } from '../../../helpers/constants';
import Leases from './Leases';
import StaticLeases from './StaticLeases/index';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import {
findActiveDhcp,
getDhcpInterfaces,
getDhcpStatus,
resetDhcp,
setDhcpConfig,
resetDhcpLeases,
toggleDhcp,
toggleLeaseModal,
} from '../../../actions';
import FormDHCPv4 from './FormDHCPv4';
import FormDHCPv6 from './FormDHCPv6';
import Interfaces from './Interfaces';
import {
calculateDhcpPlaceholdersIpv4,
calculateDhcpPlaceholdersIpv6,
subnetMaskToBitMask,
} from '../../../helpers/helpers';
import './index.css';
import { RootState } from '../../../initialState';
const Dhcp = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
processingStatus,
processingConfig,
processing,
processingInterfaces,
check,
leases,
staticLeases,
isModalOpen,
processingAdding,
processingDeleting,
processingUpdating,
processingDhcp,
v4,
v6,
interface_name: interfaceName,
enabled,
dhcp_available,
interfaces,
modalType,
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
const interface_name =
useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
const isInterfaceIncludesIpv4 =
useSelector((state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses);
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
useEffect(() => {
dispatch(getDhcpStatus());
}, []);
useEffect(() => {
if (dhcp_available) {
dispatch(getDhcpInterfaces());
}
}, [dhcp_available]);
useEffect(() => {
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
const gateway_ip = interfaces?.[interface_name]?.gateway_ip;
const v4placeholders = ipv4
? calculateDhcpPlaceholdersIpv4(ipv4, gateway_ip)
: DHCP_DESCRIPTION_PLACEHOLDERS.ipv4;
const v6placeholders = ipv6 ? calculateDhcpPlaceholdersIpv6() : DHCP_DESCRIPTION_PLACEHOLDERS.ipv6;
setIpv4Placeholders(v4placeholders);
setIpv6Placeholders(v6placeholders);
}, [interface_name]);
const clear = () => {
// eslint-disable-next-line no-alert
if (window.confirm(t('dhcp_reset'))) {
Object.values(DHCP_FORM_NAMES).forEach((formName: any) => dispatch(destroy(formName)));
dispatch(resetDhcp());
dispatch(getDhcpStatus());
}
};
const handleSubmit = (values: any) => {
dispatch(
setDhcpConfig({
interface_name,
...values,
}),
);
};
const handleReset = () => {
if (window.confirm(t('dhcp_reset_leases_confirm'))) {
dispatch(resetDhcpLeases());
}
};
const enteredSomeV4Value = Object.values(v4).some(Boolean);
const enteredSomeV6Value = Object.values(v6).some(Boolean);
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
const getToggleDhcpButton = () => {
const filledConfig =
interface_name &&
(Object.values(v4)
.every(Boolean) ||
Object.values(v6).every(Boolean));
const className = classNames('btn btn-sm', {
'btn-gray': enabled,
'btn-outline-success': !enabled,
});
const onClickDisable = () => dispatch(toggleDhcp({ enabled }));
const onClickEnable = () => {
const values = {
enabled,
interface_name,
v4: enteredSomeV4Value ? v4 : {},
v6: enteredSomeV6Value ? v6 : {},
};
dispatch(toggleDhcp(values));
};
return (
<button
type="button"
className={className}
onClick={enabled ? onClickDisable : onClickEnable}
disabled={processingDhcp || processingConfig || (!enabled && (!filledConfig || !check))}>
<Trans>{enabled ? 'dhcp_disable' : 'dhcp_enable'}</Trans>
</button>
);
};
const statusButtonClass = classNames('btn btn-sm dhcp-form__button', {
'btn-loading btn-primary': processingStatus,
'btn-outline-primary': !processingStatus,
});
const onClick = () => dispatch(findActiveDhcp(interface_name));
const toggleModal = () => dispatch(toggleLeaseModal());
const initialV4 = enteredSomeV4Value ? v4 : {};
const initialV6 = enteredSomeV6Value ? v6 : {};
if (processing || processingInterfaces) {
return <Loading />;
}
if (!processing && !dhcp_available) {
return (
<div className="text-center pt-5">
<h2>
<Trans>unavailable_dhcp</Trans>
</h2>
<h4>
<Trans>unavailable_dhcp_desc</Trans>
</h4>
</div>
);
}
const toggleDhcpButton = getToggleDhcpButton();
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const disabledLeasesButton = Boolean(
dhcp?.syncErrors ||
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
processingConfig ||
!inputtedIPv4values,
);
const cidr = inputtedIPv4values
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
: '';
return (
<>
<PageTitle title={t('dhcp_settings')} subtitle={t('dhcp_description')} containerClass="page-title--dhcp">
{toggleDhcpButton}
<button
type="button"
className={statusButtonClass}
onClick={onClick}
disabled={enabled || !interface_name || processingConfig}>
<Trans>check_dhcp_servers</Trans>
</button>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
disabled={!enteredSomeValue || processingConfig}
onClick={clear}>
<Trans>reset_settings</Trans>
</button>
</PageTitle>
{!processing && !processingInterfaces && (
<>
{!enabled &&
check &&
(check.v4.other_server.found !== STATUS_RESPONSE.NO ||
check.v6.other_server.found !== STATUS_RESPONSE.NO) && (
<div className="mb-5">
<hr />
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
</div>
)}
<Interfaces initialValues={{ interface_name: interfaceName }} />
<Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv4
onSubmit={handleSubmit}
initialValues={{ v4: initialV4 }}
processingConfig={processingConfig}
ipv4placeholders={ipv4placeholders}
/>
</div>
</Card>
<Card title={t('dhcp_ipv6_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv6
onSubmit={handleSubmit}
initialValues={{ v6: initialV6 }}
processingConfig={processingConfig}
ipv6placeholders={ipv6placeholders}
/>
</div>
</Card>
{enabled && (
<Card title={t('dhcp_leases')} bodyType="card-body box-body--settings">
<div className="row">
<div className="col">
<Leases leases={leases} disabledLeasesButton={disabledLeasesButton} />
</div>
</div>
</Card>
)}
<Card title={t('dhcp_static_leases')} bodyType="card-body box-body--settings">
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={staticLeases}
isModalOpen={isModalOpen}
modalType={modalType}
processingAdding={processingAdding}
processingDeleting={processingDeleting}
processingUpdating={processingUpdating}
cidr={cidr}
gatewayIp={dhcp?.values?.v4?.gateway_ip}
/>
<div className="btn-list mt-2">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={toggleModal}
disabled={disabledLeasesButton}>
<Trans>dhcp_add_static_lease</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standard mt-3"
onClick={handleReset}>
<Trans>dhcp_reset_leases</Trans>
</button>
</div>
</div>
</div>
</Card>
</>
)}
</>
);
};
export default Dhcp;

View File

@@ -1,123 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { renderTextareaField } from '../../../../helpers/form';
import {
trimMultilineString,
removeEmptyLines,
} from '../../../../helpers/helpers';
import { CLIENT_ID_LINK, FORM_NAME } from '../../../../helpers/constants';
const fields = [
{
id: 'allowed_clients',
title: 'access_allowed_title',
subtitle: 'access_allowed_desc',
normalizeOnBlur: removeEmptyLines,
},
{
id: 'disallowed_clients',
title: 'access_disallowed_title',
subtitle: 'access_disallowed_desc',
normalizeOnBlur: trimMultilineString,
},
{
id: 'blocked_hosts',
title: 'access_blocked_title',
subtitle: 'access_blocked_desc',
normalizeOnBlur: removeEmptyLines,
},
];
let Form = (props) => {
const {
allowedClients, handleSubmit, submitting, invalid, processingSet,
} = props;
const renderField = ({
id, title, subtitle, disabled = false, processingSet, normalizeOnBlur,
}) => <div key={id} className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor={id}>
<Trans>{title}</Trans>
{disabled && <>
<span> </span>
(<Trans>disabled</Trans>)
</>}
</label>
<div className="form__desc form__desc--top">
<Trans components={{ a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">text</a> }}>{subtitle}</Trans>
</div>
<Field
id={id}
name={id}
component={renderTextareaField}
type="text"
className="form-control form-control--textarea font-monospace"
disabled={disabled || processingSet}
normalizeOnBlur={normalizeOnBlur}
/>
</div>;
renderField.propTypes = {
id: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
disabled: PropTypes.bool,
normalizeOnBlur: PropTypes.func,
};
return (
<form onSubmit={handleSubmit}>
{
fields.map((f) => {
const props = { ...f };
if (allowedClients && f.id === 'disallowed_clients') {
props.disabled = true;
}
return renderField(props);
})
}
<div className="card-actions">
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingSet}
>
<Trans>save_config</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingSet: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
textarea: PropTypes.bool,
allowedClients: PropTypes.string,
};
const selector = formValueSelector(FORM_NAME.ACCESS);
Form = connect((state) => {
const allowedClients = selector(state, 'allowed_clients');
return {
allowedClients,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.ACCESS,
}),
])(Form);

View File

@@ -0,0 +1,137 @@
import React from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { renderTextareaField } from '../../../../helpers/form';
import { trimMultilineString, removeEmptyLines } from '../../../../helpers/helpers';
import { CLIENT_ID_LINK, FORM_NAME } from '../../../../helpers/constants';
const fields = [
{
id: 'allowed_clients',
title: 'access_allowed_title',
subtitle: 'access_allowed_desc',
normalizeOnBlur: removeEmptyLines,
},
{
id: 'disallowed_clients',
title: 'access_disallowed_title',
subtitle: 'access_disallowed_desc',
normalizeOnBlur: trimMultilineString,
},
{
id: 'blocked_hosts',
title: 'access_blocked_title',
subtitle: 'access_blocked_desc',
normalizeOnBlur: removeEmptyLines,
},
];
interface FormProps {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
invalid: boolean;
initialValues: object;
processingSet: boolean;
t: (...args: unknown[]) => string;
textarea?: boolean;
allowedClients?: string;
}
interface renderFieldProps {
id?: string;
title?: string;
subtitle?: string;
disabled?: boolean;
processingSet?: boolean;
normalizeOnBlur?: (...args: unknown[]) => unknown;
}
let Form = (props: FormProps) => {
const { allowedClients, handleSubmit, submitting, invalid, processingSet } = props;
const renderField = ({
id,
title,
subtitle,
disabled = false,
processingSet,
normalizeOnBlur,
}: renderFieldProps) => (
<div key={id} className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor={id}>
<Trans>{title}</Trans>
{disabled && (
<>
<span> </span>(<Trans>disabled</Trans>)
</>
)}
</label>
<div className="form__desc form__desc--top">
<Trans
components={{
a: (
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
text
</a>
),
}}>
{subtitle}
</Trans>
</div>
<Field
id={id}
name={id}
component={renderTextareaField}
type="text"
className="form-control form-control--textarea font-monospace"
disabled={disabled || processingSet}
normalizeOnBlur={normalizeOnBlur}
/>
</div>
);
return (
<form onSubmit={handleSubmit}>
{fields.map((f) => {
return renderField({
...f,
disabled: allowedClients && f.id === 'disallowed_clients' || false
});
})}
<div className="card-actions">
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingSet}>
<Trans>save_config</Trans>
</button>
</div>
</div>
</form>
);
};
const selector = formValueSelector(FORM_NAME.ACCESS);
Form = connect((state) => {
const allowedClients = selector(state, 'allowed_clients');
return {
allowedClients,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.ACCESS,
}),
])(Form);

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../../ui/Card';
import { setAccessList } from '../../../../actions/access';
const Access = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
processing,
processingSet,
...values
} = useSelector((state) => state.access, shallowEqual);
const handleFormSubmit = (values) => {
dispatch(setAccessList(values));
};
return (
<Card
title={t('access_title')}
subtitle={t('access_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={values}
onSubmit={handleFormSubmit}
processingSet={processingSet}
/>
</Card>
);
};
export default Access;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../../ui/Card';
import { setAccessList } from '../../../../actions/access';
import { RootState } from '../../../../initialState';
const Access = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const { processingSet, ...values } = useSelector((state: RootState) => state.access, shallowEqual);
const handleFormSubmit = (values: any) => {
dispatch(setAccessList(values));
};
return (
<Card title={t('access_title')} subtitle={t('access_desc')} bodyType="card-body box-body--settings">
<Form initialValues={values} onSubmit={handleFormSubmit} processingSet={processingSet} />
</Card>
);
};
export default Access;

View File

@@ -1,125 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { renderInputField, toNumber, CheckboxField } from '../../../../helpers/form';
import { CACHE_CONFIG_FIELDS, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
import { clearDnsCache } from '../../../../actions/dnsConfig';
const INPUTS_FIELDS = [
{
name: CACHE_CONFIG_FIELDS.cache_size,
title: 'cache_size',
description: 'cache_size_desc',
placeholder: 'enter_cache_size',
},
{
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
title: 'cache_ttl_min_override',
description: 'cache_ttl_min_override_desc',
placeholder: 'enter_cache_ttl_min_override',
},
{
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
title: 'cache_ttl_max_override',
description: 'cache_ttl_max_override_desc',
placeholder: 'enter_cache_ttl_max_override',
},
];
const Form = ({
handleSubmit, submitting, invalid,
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const { processingSetConfig } = useSelector((state) => state.dnsConfig, shallowEqual);
const {
cache_ttl_max, cache_ttl_min,
} = useSelector((state) => state.form[FORM_NAME.CACHE].values, shallowEqual);
const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max;
const handleClearCache = () => {
if (window.confirm(t('confirm_dns_cache_clear'))) {
dispatch(clearDnsCache());
}
};
return <form onSubmit={handleSubmit}>
<div className="row">
{INPUTS_FIELDS.map(({
name, title, description, placeholder, validate, min = 0, max = UINT32_RANGE.MAX,
}) => <div className="col-12" key={name}>
<div className="col-12 col-md-7 p-0">
<div className="form__group form__group--settings">
<label
htmlFor={name}
className="form__label form__label--with-desc"
>
{t(title)}
</label>
<div className="form__desc form__desc--top">{t(description)}</div>
<Field
name={name}
type="number"
component={renderInputField}
placeholder={t(placeholder)}
disabled={processingSetConfig}
className="form-control"
validate={validate}
normalizeOnBlur={replaceZeroWithEmptyString}
normalize={toNumber}
min={min}
max={max}
/>
</div>
</div>
</div>)}
{minExceedsMax && (
<span className="text-danger pl-3 pb-3">
{t('ttl_cache_validation')}
</span>
)}
</div>
<div className="row">
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<Field
name="cache_optimistic"
type="checkbox"
component={CheckboxField}
placeholder={t('cache_optimistic')}
disabled={processingSetConfig}
subtitle={t('cache_optimistic_desc')}
/>
</div>
</div>
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processingSetConfig || minExceedsMax}
>
<Trans>save_btn</Trans>
</button>
<button
type="button"
className="btn btn-outline-secondary btn-standard form__button"
onClick={handleClearCache}
>
<Trans>clear_cache</Trans>
</button>
</form>;
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
};
export default reduxForm({ form: FORM_NAME.CACHE })(Form);

View File

@@ -0,0 +1,123 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { renderInputField, toNumber, CheckboxField } from '../../../../helpers/form';
import { CACHE_CONFIG_FIELDS, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
import { clearDnsCache } from '../../../../actions/dnsConfig';
import { RootState } from '../../../../initialState';
const INPUTS_FIELDS = [
{
name: CACHE_CONFIG_FIELDS.cache_size,
title: 'cache_size',
description: 'cache_size_desc',
placeholder: 'enter_cache_size',
},
{
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
title: 'cache_ttl_min_override',
description: 'cache_ttl_min_override_desc',
placeholder: 'enter_cache_ttl_min_override',
},
{
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
title: 'cache_ttl_max_override',
description: 'cache_ttl_max_override_desc',
placeholder: 'enter_cache_ttl_max_override',
},
];
interface CacheFormProps {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
invalid: boolean;
}
const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
const { cache_ttl_max, cache_ttl_min } = useSelector(
(state: RootState) => state.form[FORM_NAME.CACHE].values,
shallowEqual,
);
const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max;
const handleClearCache = () => {
if (window.confirm(t('confirm_dns_cache_clear'))) {
dispatch(clearDnsCache());
}
};
return (
<form onSubmit={handleSubmit}>
<div className="row">
{INPUTS_FIELDS.map(({ name, title, description, placeholder }) => (
<div className="col-12" key={name}>
<div className="col-12 col-md-7 p-0">
<div className="form__group form__group--settings">
<label htmlFor={name} className="form__label form__label--with-desc">
{t(title)}
</label>
<div className="form__desc form__desc--top">{t(description)}</div>
<Field
name={name}
type="number"
component={renderInputField}
placeholder={t(placeholder)}
disabled={processingSetConfig}
className="form-control"
normalizeOnBlur={replaceZeroWithEmptyString}
normalize={toNumber}
min={0}
max={UINT32_RANGE.MAX}
/>
</div>
</div>
</div>
))}
{minExceedsMax && <span className="text-danger pl-3 pb-3">{t('ttl_cache_validation')}</span>}
</div>
<div className="row">
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<Field
name="cache_optimistic"
type="checkbox"
component={CheckboxField}
placeholder={t('cache_optimistic')}
disabled={processingSetConfig}
subtitle={t('cache_optimistic_desc')}
/>
</div>
</div>
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processingSetConfig || minExceedsMax}>
<Trans>save_btn</Trans>
</button>
<button
type="button"
className="btn btn-outline-secondary btn-standard form__button"
onClick={handleClearCache}>
<Trans>clear_cache</Trans>
</button>
</form>
);
};
export default reduxForm({ form: FORM_NAME.CACHE })(Form);

View File

@@ -1,19 +1,24 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Card from '../../../ui/Card';
import Form from './Form';
import { setDnsConfig } from '../../../../actions/dnsConfig';
import { replaceEmptyStringsWithZeroes, replaceZeroWithEmptyString } from '../../../../helpers/helpers';
import { RootState } from '../../../../initialState';
const CacheConfig = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
cache_size, cache_ttl_max, cache_ttl_min, cache_optimistic,
} = useSelector((state) => state.dnsConfig, shallowEqual);
const { cache_size, cache_ttl_max, cache_ttl_min, cache_optimistic } = useSelector(
(state: RootState) => state.dnsConfig,
shallowEqual,
);
const handleFormSubmit = (values) => {
const handleFormSubmit = (values: any) => {
const completedFields = replaceEmptyStringsWithZeroes(values);
dispatch(setDnsConfig(completedFields));
};
@@ -23,8 +28,7 @@ const CacheConfig = () => {
title={t('dns_cache_config')}
subtitle={t('dns_cache_config_desc')}
bodyType="card-body box-body--settings"
id="dns-config"
>
id="dns-config">
<div className="form">
<Form
initialValues={{

View File

@@ -1,288 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import {
renderInputField,
renderRadioField,
renderTextareaField,
CheckboxField,
toNumber,
} from '../../../../helpers/form';
import {
validateIpv4,
validateIpv6,
validateRequiredValue,
validateIp,
validateIPv4Subnet,
validateIPv6Subnet,
} from '../../../../helpers/validators';
import { removeEmptyLines } from '../../../../helpers/helpers';
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
const checkboxes = [
{
name: 'dnssec_enabled',
placeholder: 'dnssec_enable',
subtitle: 'dnssec_enable_desc',
},
{
name: 'disable_ipv6',
placeholder: 'disable_ipv6',
subtitle: 'disable_ipv6_desc',
},
];
const customIps = [
{
description: 'blocking_ipv4_desc',
name: 'blocking_ipv4',
validateIp: validateIpv4,
},
{
description: 'blocking_ipv6_desc',
name: 'blocking_ipv6',
validateIp: validateIpv6,
},
];
const getFields = (processing, t) => Object.values(BLOCKING_MODES)
.map((mode) => (
<Field
key={mode}
name="blocking_mode"
type="radio"
component={renderRadioField}
value={mode}
placeholder={t(mode)}
disabled={processing}
/>
));
const Form = ({
handleSubmit, submitting, invalid, processing,
}) => {
const { t } = useTranslation();
const {
blocking_mode,
edns_cs_enabled,
edns_cs_use_custom,
} = useSelector((state) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {}, shallowEqual);
return <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit"
className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_desc</Trans>
</div>
<Field
name="ratelimit"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit')}
normalize={toNumber}
validate={validateRequiredValue}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
/>
</div>
</div>
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_subnet_len_ipv4"
className="form__label form__label--with-desc">
<Trans>rate_limit_subnet_len_ipv4</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_subnet_len_ipv4_desc</Trans>
</div>
<Field
name="ratelimit_subnet_len_ipv4"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit_subnet_len')}
normalize={toNumber}
validate={[validateRequiredValue, validateIPv4Subnet]}
min={0}
max={32}
/>
</div>
</div>
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_subnet_len_ipv6"
className="form__label form__label--with-desc">
<Trans>rate_limit_subnet_len_ipv6</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_subnet_len_ipv6_desc</Trans>
</div>
<Field
name="ratelimit_subnet_len_ipv6"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit_subnet_len')}
normalize={toNumber}
validate={[validateRequiredValue, validateIPv6Subnet]}
min={0}
max={128}
/>
</div>
</div>
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_whitelist"
className="form__label form__label--with-desc">
<Trans>rate_limit_whitelist</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_whitelist_desc</Trans>
</div>
<Field
name="ratelimit_whitelist"
component={renderTextareaField}
type="text"
className="form-control"
placeholder={t('rate_limit_whitelist_placeholder')}
normalizeOnBlur={removeEmptyLines}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="edns_cs_enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('edns_enable')}
disabled={processing}
subtitle={t('edns_cs_desc')}
/>
</div>
</div>
<div className="col-12 form__group form__group--inner">
<div className="form__group ">
<Field
name="edns_cs_use_custom"
type="checkbox"
component={CheckboxField}
placeholder={t('edns_use_custom_ip')}
disabled={processing || !edns_cs_enabled}
subtitle={t('edns_use_custom_ip_desc')}
/>
</div>
{edns_cs_use_custom && (<Field
name="edns_cs_custom_ip"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[validateIp, validateRequiredValue]}
/>)}
</div>
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
<div className="form__group form__group--settings">
<Field
name={name}
type="checkbox"
component={CheckboxField}
placeholder={t(placeholder)}
disabled={processing}
subtitle={t(subtitle)}
/>
</div>
</div>)}
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
{Object.values(BLOCKING_MODES)
.map((mode) => (
<li key={mode}>
<Trans>{`blocking_mode_${mode}`}</Trans>
</li>
))}
</div>
<div className="custom-controls-stacked">
{getFields(processing, t)}
</div>
</div>
</div>
{blocking_mode === BLOCKING_MODES.custom_ip && (
<>
{customIps.map(({
description,
name,
validateIp,
}) => <div className="col-12 col-sm-6" key={name}>
<div className="form__group form__group--settings">
<label className="form__label form__label--with-desc"
htmlFor={name}><Trans>{name}</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>{description}</Trans>
</div>
<Field
name={name}
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[validateIp, validateRequiredValue]}
/>
</div>
</div>)}
</>
)}
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="blocked_response_ttl"
className="form__label form__label--with-desc">
<Trans>blocked_response_ttl</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocked_response_ttl_desc</Trans>
</div>
<Field
name="blocked_response_ttl"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_blocked_response_ttl')}
normalize={toNumber}
validate={validateRequiredValue}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
/>
</div>
</div>
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing}
>
<Trans>save_btn</Trans>
</button>
</form>;
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
};
export default reduxForm({ form: FORM_NAME.BLOCKING_MODE })(Form);

View File

@@ -0,0 +1,310 @@
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import {
renderInputField,
renderRadioField,
renderTextareaField,
CheckboxField,
toNumber,
} from '../../../../helpers/form';
import {
validateIpv4,
validateIpv6,
validateRequiredValue,
validateIp,
validateIPv4Subnet,
validateIPv6Subnet,
} from '../../../../helpers/validators';
import { removeEmptyLines } from '../../../../helpers/helpers';
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
import { RootState } from '../../../../initialState';
const checkboxes = [
{
name: 'dnssec_enabled',
placeholder: 'dnssec_enable',
subtitle: 'dnssec_enable_desc',
},
{
name: 'disable_ipv6',
placeholder: 'disable_ipv6',
subtitle: 'disable_ipv6_desc',
},
];
const customIps = [
{
description: 'blocking_ipv4_desc',
name: 'blocking_ipv4',
validateIp: validateIpv4,
},
{
description: 'blocking_ipv6_desc',
name: 'blocking_ipv6',
validateIp: validateIpv6,
},
];
const getFields = (processing: any, t: any) =>
Object.values(BLOCKING_MODES)
.map((mode: any) => (
<Field
key={mode}
name="blocking_mode"
type="radio"
component={renderRadioField}
value={mode}
placeholder={t(mode)}
disabled={processing}
/>
));
interface ConfigFormProps {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
invalid: boolean;
processing?: boolean;
}
const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps) => {
const { t } = useTranslation();
const { blocking_mode, edns_cs_enabled, edns_cs_use_custom } = useSelector(
(state: RootState) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {},
shallowEqual,
);
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_desc</Trans>
</div>
<Field
name="ratelimit"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit')}
normalize={toNumber}
validate={validateRequiredValue}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
/>
</div>
</div>
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_subnet_len_ipv4" className="form__label form__label--with-desc">
<Trans>rate_limit_subnet_len_ipv4</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_subnet_len_ipv4_desc</Trans>
</div>
<Field
name="ratelimit_subnet_len_ipv4"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit_subnet_len')}
normalize={toNumber}
validate={[validateRequiredValue, validateIPv4Subnet]}
min={0}
max={32}
/>
</div>
</div>
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_subnet_len_ipv6" className="form__label form__label--with-desc">
<Trans>rate_limit_subnet_len_ipv6</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_subnet_len_ipv6_desc</Trans>
</div>
<Field
name="ratelimit_subnet_len_ipv6"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit_subnet_len')}
normalize={toNumber}
validate={[validateRequiredValue, validateIPv6Subnet]}
min={0}
max={128}
/>
</div>
</div>
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_whitelist" className="form__label form__label--with-desc">
<Trans>rate_limit_whitelist</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_whitelist_desc</Trans>
</div>
<Field
name="ratelimit_whitelist"
component={renderTextareaField}
type="text"
className="form-control"
placeholder={t('rate_limit_whitelist_placeholder')}
normalizeOnBlur={removeEmptyLines}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="edns_cs_enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('edns_enable')}
disabled={processing}
subtitle={t('edns_cs_desc')}
/>
</div>
</div>
<div className="col-12 form__group form__group--inner">
<div className="form__group ">
<Field
name="edns_cs_use_custom"
type="checkbox"
component={CheckboxField}
placeholder={t('edns_use_custom_ip')}
disabled={processing || !edns_cs_enabled}
subtitle={t('edns_use_custom_ip_desc')}
/>
</div>
{edns_cs_use_custom && (
<Field
name="edns_cs_custom_ip"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[validateIp, validateRequiredValue]}
/>
)}
</div>
{checkboxes.map(({ name, placeholder, subtitle }) => (
<div className="col-12" key={name}>
<div className="form__group form__group--settings">
<Field
name={name}
type="checkbox"
component={CheckboxField}
placeholder={t(placeholder)}
disabled={processing}
subtitle={t(subtitle)}
/>
</div>
</div>
))}
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
{Object.values(BLOCKING_MODES)
.map((mode: any) => (
<li key={mode}>
<Trans>{`blocking_mode_${mode}`}</Trans>
</li>
))}
</div>
<div className="custom-controls-stacked">{getFields(processing, t)}</div>
</div>
</div>
{blocking_mode === BLOCKING_MODES.custom_ip && (
<>
{customIps.map(({ description, name, validateIp }) => (
<div className="col-12 col-sm-6" key={name}>
<div className="form__group form__group--settings">
<label className="form__label form__label--with-desc" htmlFor={name}>
<Trans>{name}</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>{description}</Trans>
</div>
<Field
name={name}
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[validateIp, validateRequiredValue]}
/>
</div>
</div>
))}
</>
)}
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="blocked_response_ttl" className="form__label form__label--with-desc">
<Trans>blocked_response_ttl</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocked_response_ttl_desc</Trans>
</div>
<Field
name="blocked_response_ttl"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_blocked_response_ttl')}
normalize={toNumber}
validate={validateRequiredValue}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
/>
</div>
</div>
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing}>
<Trans>save_btn</Trans>
</button>
</form>
);
};
export default reduxForm<Record<string, any>, Omit<ConfigFormProps, 'invalid' | 'submitting' | 'handleSubmit'>>({
form: FORM_NAME.BLOCKING_MODE,
})(Form);

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Card from '../../../ui/Card';
import Form from './Form';
import { setDnsConfig } from '../../../../actions/dnsConfig';
import { RootState } from '../../../../initialState';
const Config = () => {
const { t } = useTranslation();
@@ -23,18 +26,14 @@ const Config = () => {
dnssec_enabled,
disable_ipv6,
processingSetConfig,
} = useSelector((state) => state.dnsConfig, shallowEqual);
} = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
const handleFormSubmit = (values) => {
const handleFormSubmit = (values: any) => {
dispatch(setDnsConfig(values));
};
return (
<Card
title={t('dns_config')}
bodyType="card-body box-body--settings"
id="dns-config"
>
<Card title={t('dns_config')} bodyType="card-body box-body--settings" id="dns-config">
<div className="form">
<Form
initialValues={{

View File

@@ -1,169 +1,167 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import { COMMENT_LINE_DEFAULT_TOKEN } from '../../../../helpers/constants';
const Examples = (props) => (
interface ExamplesProps {
t: (...args: unknown[]) => string;
}
const Examples = (props: ExamplesProps) => (
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>94.140.14.140</code>, <code>2a10:50c0::1:ff</code>: {props.t('example_upstream_regular')}
</li>
<li>
<code>94.140.14.140:53</code>, <code>[2a10:50c0::1:ff]:53</code>: {props.t('example_upstream_regular_port')}
<code>94.140.14.140:53</code>, <code>[2a10:50c0::1:ff]:53</code>:{' '}
{props.t('example_upstream_regular_port')}
</li>
<li>
<code>udp://unfiltered.adguard-dns.com</code>: <Trans>example_upstream_udp</Trans>
</li>
<li>
<code>tcp://94.140.14.140</code>, <code>tcp://[2a10:50c0::1:ff]</code>: <Trans>example_upstream_tcp</Trans>
<code>tcp://94.140.14.140</code>, <code>tcp://[2a10:50c0::1:ff]</code>:{' '}
<Trans>example_upstream_tcp</Trans>
</li>
<li>
<code>tcp://94.140.14.140:53</code>, <code>tcp://[2a10:50c0::1:ff]:53</code>: <Trans>example_upstream_tcp_port</Trans>
<code>tcp://94.140.14.140:53</code>, <code>tcp://[2a10:50c0::1:ff]:53</code>:{' '}
<Trans>example_upstream_tcp_port</Trans>
</li>
<li>
<code>tcp://unfiltered.adguard-dns.com</code>: <Trans>example_upstream_tcp_hostname</Trans>
</li>
<li>
<code>tls://unfiltered.adguard-dns.com</code>: <Trans
<code>tls://unfiltered.adguard-dns.com</code>:{' '}
<Trans
components={[
<a
href="https://en.wikipedia.org/wiki/DNS_over_TLS"
target="_blank"
rel="noopener noreferrer"
key="0"
>
key="0">
DNS-over-TLS
</a>,
]}
>
]}>
example_upstream_dot
</Trans>
</li>
<li>
<code>https://unfiltered.adguard-dns.com/dns-query</code>: <Trans
<code>https://unfiltered.adguard-dns.com/dns-query</code>:{' '}
<Trans
components={[
<a
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
target="_blank"
rel="noopener noreferrer"
key="0"
>
key="0">
DNS-over-HTTPS
</a>,
]}
>
]}>
example_upstream_doh
</Trans>
</li>
<li>
<code>h3://unfiltered.adguard-dns.com/dns-query</code>: <Trans
<code>h3://unfiltered.adguard-dns.com/dns-query</code>:{' '}
<Trans
components={[
<a
href="https://en.wikipedia.org/wiki/HTTP/3"
target="_blank"
rel="noopener noreferrer"
key="0"
>
key="0">
HTTP/3
</a>,
]}
>
]}>
example_upstream_doh3
</Trans>
</li>
<li>
<code>quic://unfiltered.adguard-dns.com</code>: <Trans
<code>quic://unfiltered.adguard-dns.com</code>:{' '}
<Trans
components={[
<a
href="https://datatracker.ietf.org/doc/html/rfc9250"
target="_blank"
rel="noopener noreferrer"
key="0"
>
key="0">
DNS-over-QUIC
</a>,
]}
>
]}>
example_upstream_doq
</Trans>
</li>
<li>
<code>sdns://...</code>: <Trans
<code>sdns://...</code>:{' '}
<Trans
components={[
<a
href="https://dnscrypt.info/stamps/"
target="_blank"
rel="noopener noreferrer"
key="0"
>
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer" key="0">
DNS Stamps
</a>,
<a
href="https://dnscrypt.info/"
target="_blank"
rel="noopener noreferrer"
key="1"
>
<a href="https://dnscrypt.info/" target="_blank" rel="noopener noreferrer" key="1">
DNSCrypt
</a>,
<a
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
target="_blank"
rel="noopener noreferrer"
key="2"
>
key="2">
DNS-over-HTTPS
</a>,
]}
>
]}>
example_upstream_sdns
</Trans>
</li>
<li>
<code>[/example.local/]94.140.14.140</code>: <Trans
<code>[/example.local/]94.140.14.140</code>:{' '}
<Trans
components={[
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
target="_blank"
rel="noopener noreferrer"
key="0"
>
key="0">
Link
</a>,
]}
>
]}>
example_upstream_reserved
</Trans>
</li>
<li>
<code>[/example.local/]94.140.14.140 2a10:50c0::1:ff</code>: <Trans
<code>[/example.local/]94.140.14.140 2a10:50c0::1:ff</code>:{' '}
<Trans
components={[
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
target="_blank"
rel="noopener noreferrer"
key="0"
>
key="0">
Link
</a>,
]}
>
]}>
example_multiple_upstreams_reserved
</Trans>
</li>
<li>
<code>{COMMENT_LINE_DEFAULT_TOKEN} comment</code>: <Trans>
example_upstream_comment
</Trans>
<code>{COMMENT_LINE_DEFAULT_TOKEN} comment</code>: <Trans>example_upstream_comment</Trans>
</li>
</ol>
</div>
);
Examples.propTypes = {
t: PropTypes.func.isRequired,
};
export default withTranslation()(Examples);

View File

@@ -1,317 +0,0 @@
import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import classnames from 'classnames';
import Examples from './Examples';
import { renderRadioField, renderTextareaField, CheckboxField } from '../../../../helpers/form';
import {
DNS_REQUEST_OPTIONS,
FORM_NAME,
UPSTREAM_CONFIGURATION_WIKI_LINK,
} from '../../../../helpers/constants';
import { testUpstreamWithFormValues } from '../../../../actions';
import { removeEmptyLines, trimLinesAndRemoveEmpty } from '../../../../helpers/helpers';
import { getTextareaCommentsHighlight, syncScroll } from '../../../../helpers/highlightTextareaComments';
import '../../../ui/texareaCommentsHighlight.css';
const UPSTREAM_DNS_NAME = 'upstream_dns';
const UPSTREAM_MODE_NAME = 'upstream_mode';
const renderField = ({
name, component, type, className, placeholder,
subtitle, value, normalizeOnBlur, containerClass, onScroll,
}) => {
const { t } = useTranslation();
const processingTestUpstream = useSelector((state) => state.settings.processingTestUpstream);
const processingSetConfig = useSelector((state) => state.dnsConfig.processingSetConfig);
return (
<div
key={placeholder}
className={classnames('col-12 mb-4', containerClass)}
>
<Field
id={name}
value={value}
name={name}
component={component}
type={type}
className={className}
placeholder={t(placeholder)}
subtitle={t(subtitle)}
disabled={processingSetConfig || processingTestUpstream}
normalizeOnBlur={normalizeOnBlur}
onScroll={onScroll}
/>
</div>
);
};
renderField.propTypes = {
name: PropTypes.string.isRequired,
component: PropTypes.element.isRequired,
type: PropTypes.string.isRequired,
className: PropTypes.string,
placeholder: PropTypes.string.isRequired,
subtitle: PropTypes.string,
value: PropTypes.string,
normalizeOnBlur: PropTypes.func,
containerClass: PropTypes.string,
onScroll: PropTypes.func,
};
const renderTextareaWithHighlightField = (props) => {
const upstream_dns = useSelector((store) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
const upstream_dns_file = useSelector((state) => state.dnsConfig.upstream_dns_file);
const ref = useRef(null);
const onScroll = (e) => syncScroll(e, ref);
return <>
{renderTextareaField({
...props,
disabled: !!upstream_dns_file,
onScroll,
normalizeOnBlur: trimLinesAndRemoveEmpty,
})}
{getTextareaCommentsHighlight(ref, upstream_dns)}
</>;
};
renderTextareaWithHighlightField.propTypes = {
className: PropTypes.string.isRequired,
disabled: PropTypes.bool,
id: PropTypes.string.isRequired,
input: PropTypes.object,
meta: PropTypes.object,
normalizeOnBlur: PropTypes.func,
onScroll: PropTypes.func,
placeholder: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
const INPUT_FIELDS = [
{
name: UPSTREAM_MODE_NAME,
type: 'radio',
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
component: renderRadioField,
subtitle: 'load_balancing_desc',
placeholder: 'load_balancing',
},
{
name: UPSTREAM_MODE_NAME,
type: 'radio',
value: DNS_REQUEST_OPTIONS.PARALLEL,
component: renderRadioField,
subtitle: 'upstream_parallel',
placeholder: 'parallel_requests',
},
{
name: UPSTREAM_MODE_NAME,
type: 'radio',
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
component: renderRadioField,
subtitle: 'fastest_addr_desc',
placeholder: 'fastest_addr',
},
];
const Form = ({
submitting, invalid, handleSubmit,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const upstream_dns = useSelector((store) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
const processingTestUpstream = useSelector((state) => state.settings.processingTestUpstream);
const processingSetConfig = useSelector((state) => state.dnsConfig.processingSetConfig);
const defaultLocalPtrUpstreams = useSelector(
(state) => state.dnsConfig.default_local_ptr_upstreams,
);
const handleUpstreamTest = () => dispatch(testUpstreamWithFormValues());
const testButtonClass = classnames('btn btn-primary btn-standard mr-2', {
'btn-loading': processingTestUpstream,
});
const components = {
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank"
rel="noopener noreferrer" />,
};
return <form onSubmit={handleSubmit} className="form--upstream">
<div className="row">
<label className="col form__label" htmlFor={UPSTREAM_DNS_NAME}>
<Trans components={components}>upstream_dns_help</Trans>
{' '}
<Trans components={[
<a
href="https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home"
target="_blank"
rel="noopener noreferrer"
key="0"
>
DNS providers
</a>,
]}>
dns_providers
</Trans>
</label>
<div className="col-12 mb-4">
<div className="text-edit-container">
<Field
id={UPSTREAM_DNS_NAME}
name={UPSTREAM_DNS_NAME}
component={renderTextareaWithHighlightField}
type="text"
className="form-control form-control--textarea font-monospace text-input"
placeholder={t('upstream_dns')}
disabled={processingSetConfig || processingTestUpstream}
normalizeOnBlur={removeEmptyLines}
/>
</div>
</div>
{INPUT_FIELDS.map(renderField)}
<div className="col-12">
<Examples />
<hr />
</div>
<div className="col-12">
<label
className="form__label form__label--with-desc"
htmlFor="fallback_dns"
>
<Trans>fallback_dns_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>fallback_dns_desc</Trans>
</div>
<Field
id="fallback_dns"
name="fallback_dns"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('fallback_dns_placeholder')}
disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/>
</div>
<div className="col-12">
<hr />
</div>
<div className="col-12 mb-2">
<label
className="form__label form__label--with-desc"
htmlFor="bootstrap_dns"
>
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>bootstrap_dns_desc</Trans>
</div>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('bootstrap_dns')}
disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/>
</div>
<div className="col-12">
<hr />
</div>
<div className="col-12">
<label
className="form__label form__label--with-desc"
htmlFor="local_ptr"
>
<Trans>local_ptr_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>local_ptr_desc</Trans>
</div>
<div className="form__desc form__desc--top">
{/** TODO: Add internazionalization for "" */}
{defaultLocalPtrUpstreams?.length > 0 ? (
<Trans values={{ ip: defaultLocalPtrUpstreams.map((s) => `"${s}"`).join(', ') }}>local_ptr_default_resolver</Trans>
) : (
<Trans>local_ptr_no_default_resolver</Trans>
)}
</div>
<Field
id="local_ptr_upstreams"
name="local_ptr_upstreams"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('local_ptr_placeholder')}
disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/>
<div className="mt-4">
<Field
name="use_private_ptr_resolvers"
type="checkbox"
component={CheckboxField}
placeholder={t('use_private_ptr_resolvers_title')}
subtitle={t('use_private_ptr_resolvers_desc')}
disabled={processingSetConfig}
/>
</div>
</div>
<div className="col-12">
<hr />
</div>
<div className="col-12 mb-4">
<Field
name="resolve_clients"
type="checkbox"
component={CheckboxField}
placeholder={t('resolve_clients_title')}
subtitle={t('resolve_clients_desc')}
disabled={processingSetConfig}
/>
</div>
</div>
<div className="card-actions">
<div className="btn-list">
<button
type="button"
className={testButtonClass}
onClick={handleUpstreamTest}
disabled={!upstream_dns || processingTestUpstream}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting || invalid || processingSetConfig || processingTestUpstream
}
>
<Trans>apply_btn</Trans>
</button>
</div>
</div>
</form>;
};
Form.propTypes = {
handleSubmit: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
initialValues: PropTypes.object,
upstream_dns: PropTypes.string,
fallback_dns: PropTypes.string,
bootstrap_dns: PropTypes.string,
};
export default reduxForm({ form: FORM_NAME.UPSTREAM })(Form);

View File

@@ -0,0 +1,338 @@
import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import classnames from 'classnames';
import Examples from './Examples';
import { renderRadioField, renderTextareaField, CheckboxField } from '../../../../helpers/form';
import { DNS_REQUEST_OPTIONS, FORM_NAME, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';
import { testUpstreamWithFormValues } from '../../../../actions';
import { removeEmptyLines, trimLinesAndRemoveEmpty } from '../../../../helpers/helpers';
import { getTextareaCommentsHighlight, syncScroll } from '../../../../helpers/highlightTextareaComments';
import '../../../ui/texareaCommentsHighlight.css';
import { RootState } from '../../../../initialState';
const UPSTREAM_DNS_NAME = 'upstream_dns';
const UPSTREAM_MODE_NAME = 'upstream_mode';
interface renderFieldProps {
name: string;
component: any;
type: string;
className?: string;
placeholder: string;
subtitle?: string;
value?: string;
normalizeOnBlur?: (...args: unknown[]) => unknown;
containerClass?: string;
onScroll?: (...args: unknown[]) => unknown;
}
const renderField = ({
name,
component,
type,
className,
placeholder,
subtitle,
value,
normalizeOnBlur,
containerClass,
onScroll,
}: renderFieldProps) => {
const { t } = useTranslation();
const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);
const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);
return (
<div key={placeholder} className={classnames('col-12 mb-4', containerClass)}>
<Field
id={name}
value={value}
name={name}
component={component}
type={type}
className={className}
placeholder={t(placeholder)}
subtitle={t(subtitle)}
disabled={processingSetConfig || processingTestUpstream}
normalizeOnBlur={normalizeOnBlur}
onScroll={onScroll}
/>
</div>
);
};
interface renderTextareaWithHighlightFieldProps {
className: string;
disabled?: boolean;
id: string;
input?: object;
meta?: object;
normalizeOnBlur?: (...args: unknown[]) => unknown;
onScroll?: (...args: unknown[]) => unknown;
placeholder: string;
type: string;
}
const renderTextareaWithHighlightField = (props: renderTextareaWithHighlightFieldProps) => {
const upstream_dns = useSelector((store: RootState) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);
const ref = useRef(null);
const onScroll = (e: any) => syncScroll(e, ref);
return (
<>
{renderTextareaField({
...props,
disabled: !!upstream_dns_file,
onScroll,
normalizeOnBlur: trimLinesAndRemoveEmpty,
})}
{getTextareaCommentsHighlight(ref, upstream_dns)}
</>
);
};
const INPUT_FIELDS = [
{
name: UPSTREAM_MODE_NAME,
type: 'radio',
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
component: renderRadioField,
subtitle: 'load_balancing_desc',
placeholder: 'load_balancing',
},
{
name: UPSTREAM_MODE_NAME,
type: 'radio',
value: DNS_REQUEST_OPTIONS.PARALLEL,
component: renderRadioField,
subtitle: 'upstream_parallel',
placeholder: 'parallel_requests',
},
{
name: UPSTREAM_MODE_NAME,
type: 'radio',
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
component: renderRadioField,
subtitle: 'fastest_addr_desc',
placeholder: 'fastest_addr',
},
];
interface FormProps {
handleSubmit?: (...args: unknown[]) => string;
submitting?: boolean;
invalid?: boolean;
initialValues?: object;
upstream_dns?: string;
fallback_dns?: string;
bootstrap_dns?: string;
}
const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const upstream_dns = useSelector((store: RootState) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);
const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);
const defaultLocalPtrUpstreams = useSelector((state: RootState) => state.dnsConfig.default_local_ptr_upstreams);
const handleUpstreamTest = () => dispatch(testUpstreamWithFormValues());
const testButtonClass = classnames('btn btn-primary btn-standard mr-2', {
'btn-loading': processingTestUpstream,
});
const components = {
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
};
return (
<form onSubmit={handleSubmit} className="form--upstream">
<div className="row">
<label className="col form__label" htmlFor={UPSTREAM_DNS_NAME}>
<Trans components={components}>upstream_dns_help</Trans>{' '}
<Trans
components={[
<a
href="https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home"
target="_blank"
rel="noopener noreferrer"
key="0">
DNS providers
</a>,
]}>
dns_providers
</Trans>
</label>
<div className="col-12 mb-4">
<div className="text-edit-container">
<Field
id={UPSTREAM_DNS_NAME}
name={UPSTREAM_DNS_NAME}
component={renderTextareaWithHighlightField}
type="text"
className="form-control form-control--textarea font-monospace text-input"
placeholder={t('upstream_dns')}
disabled={processingSetConfig || processingTestUpstream}
normalizeOnBlur={removeEmptyLines}
/>
</div>
</div>
{INPUT_FIELDS.map(renderField)}
<div className="col-12">
<Examples />
<hr />
</div>
<div className="col-12">
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
<Trans>fallback_dns_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>fallback_dns_desc</Trans>
</div>
<Field
id="fallback_dns"
name="fallback_dns"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('fallback_dns_placeholder')}
disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/>
</div>
<div className="col-12">
<hr />
</div>
<div className="col-12 mb-2">
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>bootstrap_dns_desc</Trans>
</div>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('bootstrap_dns')}
disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/>
</div>
<div className="col-12">
<hr />
</div>
<div className="col-12">
<label className="form__label form__label--with-desc" htmlFor="local_ptr">
<Trans>local_ptr_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>local_ptr_desc</Trans>
</div>
<div className="form__desc form__desc--top">
{/** TODO: Add internazionalization for "" */}
{defaultLocalPtrUpstreams?.length > 0 ? (
<Trans values={{ ip: defaultLocalPtrUpstreams.map((s: any) => `"${s}"`).join(', ') }}>
local_ptr_default_resolver
</Trans>
) : (
<Trans>local_ptr_no_default_resolver</Trans>
)}
</div>
<Field
id="local_ptr_upstreams"
name="local_ptr_upstreams"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('local_ptr_placeholder')}
disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/>
<div className="mt-4">
<Field
name="use_private_ptr_resolvers"
type="checkbox"
component={CheckboxField}
placeholder={t('use_private_ptr_resolvers_title')}
subtitle={t('use_private_ptr_resolvers_desc')}
disabled={processingSetConfig}
/>
</div>
</div>
<div className="col-12">
<hr />
</div>
<div className="col-12 mb-4">
<Field
name="resolve_clients"
type="checkbox"
component={CheckboxField}
placeholder={t('resolve_clients_title')}
subtitle={t('resolve_clients_desc')}
disabled={processingSetConfig}
/>
</div>
</div>
<div className="card-actions">
<div className="btn-list">
<button
type="button"
className={testButtonClass}
onClick={handleUpstreamTest}
disabled={!upstream_dns || processingTestUpstream}>
<Trans>test_upstream_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingSetConfig || processingTestUpstream}>
<Trans>apply_btn</Trans>
</button>
</div>
</div>
</form>
);
};
export default reduxForm({ form: FORM_NAME.UPSTREAM })(Form);

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../../ui/Card';
import { setDnsConfig } from '../../../../actions/dnsConfig';
import { RootState } from '../../../../initialState';
const Upstream = () => {
const { t } = useTranslation();
@@ -16,11 +19,11 @@ const Upstream = () => {
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
} = useSelector((state) => state.dnsConfig, shallowEqual);
} = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
const upstream_dns_file = useSelector((state) => state.dnsConfig.upstream_dns_file);
const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);
const handleSubmit = (values) => {
const handleSubmit = (values: any) => {
const {
fallback_dns,
bootstrap_dns,
@@ -44,29 +47,30 @@ const Upstream = () => {
dispatch(setDnsConfig(dnsConfig));
};
const upstreamDns = upstream_dns_file ? t('upstream_dns_configured_in_file', { path: upstream_dns_file }) : upstream_dns;
const upstreamDns = upstream_dns_file
? t('upstream_dns_configured_in_file', { path: upstream_dns_file })
: upstream_dns;
return <Card
title={t('upstream_dns')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Form
initialValues={{
upstream_dns: upstreamDns,
fallback_dns,
bootstrap_dns,
upstream_mode,
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
}}
onSubmit={handleSubmit}
/>
return (
<Card title={t('upstream_dns')} bodyType="card-body box-body--settings">
<div className="row">
<div className="col">
<Form
initialValues={{
upstream_dns: upstreamDns,
fallback_dns,
bootstrap_dns,
upstream_mode,
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
}}
onSubmit={handleSubmit}
/>
</div>
</div>
</div>
</Card>;
</Card>
);
};
export default Upstream;

View File

@@ -2,20 +2,29 @@ import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Upstream from './Upstream';
import Access from './Access';
import Config from './Config';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import CacheConfig from './Cache';
import { getDnsConfig } from '../../../actions/dnsConfig';
import { getAccessList } from '../../../actions/access';
import { RootState } from '../../../initialState';
const Dns = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const processing = useSelector((state) => state.access.processing);
const processingGetConfig = useSelector((state) => state.dnsConfig.processingGetConfig);
const processing = useSelector((state: RootState) => state.access.processing);
const processingGetConfig = useSelector((state: RootState) => state.dnsConfig.processingGetConfig);
const isDataLoading = processing || processingGetConfig;
@@ -24,17 +33,24 @@ const Dns = () => {
dispatch(getDnsConfig());
}, []);
return <>
<PageTitle title={t('dns_settings')} />
{isDataLoading
? <Loading />
: <>
<Upstream />
<Config />
<CacheConfig />
<Access />
</>}
</>;
return (
<>
<PageTitle title={t('dns_settings')} />
{isDataLoading ? (
<Loading />
) : (
<>
<Upstream />
<Config />
<CacheConfig />
<Access />
</>
)}
</>
);
};
export default Dns;

View File

@@ -1,31 +1,27 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
import format from 'date-fns/format';
import { EMPTY_DATE } from '../../../helpers/constants';
const CertificateStatus = ({
validChain,
validCert,
subject,
issuer,
notAfter,
dnsNames,
}) => (
interface CertificateStatusProps {
validChain: boolean;
validCert: boolean;
subject?: string;
issuer?: string;
notAfter?: string;
dnsNames?: string[];
}
const CertificateStatus = ({ validChain, validCert, subject, issuer, notAfter, dnsNames }: CertificateStatusProps) => (
<Fragment>
<div className="form__label form__label--bold">
<Trans>encryption_status</Trans>:
</div>
<ul className="encryption__list">
<li
className={validChain ? 'text-success' : 'text-danger'}
>
{validChain ? (
<Trans>encryption_chain_valid</Trans>
) : (
<Trans>encryption_chain_invalid</Trans>
)}
<li className={validChain ? 'text-success' : 'text-danger'}>
{validChain ? <Trans>encryption_chain_valid</Trans> : <Trans>encryption_chain_invalid</Trans>}
</li>
{validCert && (
<Fragment>
@@ -59,13 +55,4 @@ const CertificateStatus = ({
</Fragment>
);
CertificateStatus.propTypes = {
validChain: PropTypes.bool.isRequired,
validCert: PropTypes.bool.isRequired,
subject: PropTypes.string,
issuer: PropTypes.string,
notAfter: PropTypes.string,
dnsNames: PropTypes.arrayOf(PropTypes.string),
};
export default withTranslation()(CertificateStatus);

View File

@@ -1,32 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { renderInputField, CheckboxField, renderRadioField, toNumber } from '../../../helpers/form';
import {
renderInputField,
CheckboxField,
renderRadioField,
toNumber,
} from '../../../helpers/form';
import {
validateServerName, validateIsSafePort, validatePort, validatePortQuic, validatePortTLS, validatePlainDns,
validateServerName,
validateIsSafePort,
validatePort,
validatePortQuic,
validatePortTLS,
validatePlainDns,
} from '../../../helpers/validators';
import i18n from '../../../i18n';
import KeyStatus from './KeyStatus';
import CertificateStatus from './CertificateStatus';
import {
DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT, ENCRYPTION_SOURCE,
DNS_OVER_QUIC_PORT,
DNS_OVER_TLS_PORT,
FORM_NAME,
STANDARD_HTTPS_PORT,
ENCRYPTION_SOURCE,
} from '../../../helpers/constants';
const validate = (values) => {
const errors = {};
const validate = (values: any) => {
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
if (values.port_dns_over_tls && values.port_https) {
if (values.port_dns_over_tls === values.port_https) {
errors.port_dns_over_tls = i18n.t('form_error_equal');
errors.port_https = i18n.t('form_error_equal');
}
}
@@ -34,7 +41,7 @@ const validate = (values) => {
return errors;
};
const clearFields = (change, setTlsConfig, validateTlsConfig, t) => {
const clearFields = (change: any, setTlsConfig: any, validateTlsConfig: any, t: any) => {
const fields = {
private_key: '',
certificate_chain: '',
@@ -52,13 +59,14 @@ const clearFields = (change, setTlsConfig, validateTlsConfig, t) => {
// eslint-disable-next-line no-alert
if (window.confirm(t('encryption_reset'))) {
Object.keys(fields)
.forEach((field) => change(field, fields[field]));
setTlsConfig(fields);
validateTlsConfig(fields);
}
};
const validationMessage = (warningValidation, isWarning) => {
const validationMessage = (warningValidation: any, isWarning: any) => {
if (!warningValidation) {
return null;
}
@@ -66,7 +74,9 @@ const validationMessage = (warningValidation, isWarning) => {
if (isWarning) {
return (
<div className="col-12">
<p><Trans>encryption_warning</Trans>: {warningValidation}</p>
<p>
<Trans>encryption_warning</Trans>: {warningValidation}
</p>
</div>
);
}
@@ -78,7 +88,41 @@ const validationMessage = (warningValidation, isWarning) => {
);
};
let Form = (props) => {
interface FormProps {
handleSubmit: (...args: unknown[]) => string;
handleChange?: (...args: unknown[]) => unknown;
isEnabled: boolean;
servePlainDns: boolean;
certificateChain: string;
privateKey: string;
certificatePath: string;
privateKeyPath: string;
change: (...args: unknown[]) => unknown;
submitting: boolean;
invalid: boolean;
initialValues: object;
processingConfig: boolean;
processingValidate: boolean;
status_key?: string;
not_after?: string;
warning_validation?: string;
valid_chain?: boolean;
valid_key?: boolean;
valid_cert?: boolean;
valid_pair?: boolean;
dns_names?: string[];
key_type?: string;
issuer?: string;
subject?: string;
t: (...args: unknown[]) => string;
setTlsConfig: (...args: unknown[]) => unknown;
validateTlsConfig: (...args: unknown[]) => unknown;
certificateSource?: string;
privateKeySource?: string;
privateKeySaved?: boolean;
}
let Form = (props: FormProps) => {
const {
t,
handleSubmit,
@@ -137,9 +181,11 @@ let Form = (props) => {
onChange={handleChange}
/>
</div>
<div className="form__desc">
<Trans>encryption_enable_desc</Trans>
</div>
<div className="form__group mb-3 mt-5">
<Field
name="serve_plain_dns"
@@ -150,16 +196,20 @@ let Form = (props) => {
validate={validatePlainDns}
/>
</div>
<div className="form__desc">
<Trans>encryption_plain_dns_desc</Trans>
</div>
<hr />
</div>
<div className="col-12">
<label className="form__label" htmlFor="server_name">
<Trans>encryption_server</Trans>
</label>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<Field
@@ -173,11 +223,13 @@ let Form = (props) => {
disabled={!isEnabled}
validate={validateServerName}
/>
<div className="form__desc">
<Trans>encryption_server_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<Field
@@ -188,18 +240,21 @@ let Form = (props) => {
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_redirect_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_https">
<Trans>encryption_https</Trans>
</label>
<Field
id="port_https"
name="port_https"
@@ -212,16 +267,19 @@ let Form = (props) => {
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_https_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_dns_over_tls">
<Trans>encryption_dot</Trans>
</label>
<Field
id="port_dns_over_tls"
name="port_dns_over_tls"
@@ -234,16 +292,19 @@ let Form = (props) => {
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_dot_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_dns_over_quic">
<Trans>encryption_doq</Trans>
</label>
<Field
id="port_dns_over_quic"
name="port_dns_over_quic"
@@ -256,30 +317,35 @@ let Form = (props) => {
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_doq_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label
className="form__label form__label--with-desc form__label--bold"
htmlFor="certificate_chain"
>
htmlFor="certificate_chain">
<Trans>encryption_certificates</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans
values={{ link: 'letsencrypt.org' }}
components={[
<a target="_blank" rel="noopener noreferrer" href="https://letsencrypt.org/" key="0">
<a
target="_blank"
rel="noopener noreferrer"
href="https://letsencrypt.org/"
key="0">
link
</a>,
]}
>
]}>
encryption_certificates_desc
</Trans>
</div>
@@ -295,6 +361,7 @@ let Form = (props) => {
placeholder={t('encryption_certificates_source_path')}
disabled={!isEnabled}
/>
<Field
name="certificate_source"
component={renderRadioField}
@@ -332,6 +399,7 @@ let Form = (props) => {
/>
)}
</div>
<div className="form__status">
{(certificateChain || certificatePath) && (
<CertificateStatus
@@ -346,6 +414,7 @@ let Form = (props) => {
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings mt-3">
@@ -364,6 +433,7 @@ let Form = (props) => {
placeholder={t('encryption_key_source_path')}
disabled={!isEnabled}
/>
<Field
name="key_source"
component={renderRadioField}
@@ -396,7 +466,7 @@ let Form = (props) => {
component={CheckboxField}
disabled={!isEnabled}
placeholder={t('use_saved_key')}
onChange={(event) => {
onChange={(event: any) => {
if (event.target.checked) {
change('private_key', '');
}
@@ -405,6 +475,7 @@ let Form = (props) => {
}
}}
/>,
<Field
id="private_key"
key="private_key"
@@ -418,29 +489,24 @@ let Form = (props) => {
/>,
]}
</div>
<div className="form__status">
{(privateKey || privateKeyPath) && (
<KeyStatus validKey={valid_key} keyType={key_type} />
)}
{(privateKey || privateKeyPath) && <KeyStatus validKey={valid_key} keyType={key_type} />}
</div>
</div>
{validationMessage(warning_validation, isWarning)}
</div>
<div className="btn-list mt-2">
<button
type="submit"
disabled={isDisabled}
className="btn btn-success btn-standart"
>
<button type="submit" disabled={isDisabled} className="btn btn-success btn-standart">
<Trans>save_config</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standart"
disabled={submitting || processingConfig}
onClick={() => clearFields(change, setTlsConfig, validateTlsConfig, t)}
>
onClick={() => clearFields(change, setTlsConfig, validateTlsConfig, t)}>
<Trans>reset_settings</Trans>
</button>
</div>
@@ -448,40 +514,6 @@ let Form = (props) => {
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isEnabled: PropTypes.bool.isRequired,
servePlainDns: PropTypes.bool.isRequired,
certificateChain: PropTypes.string.isRequired,
privateKey: PropTypes.string.isRequired,
certificatePath: PropTypes.string.isRequired,
privateKeyPath: PropTypes.string.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
processingValidate: PropTypes.bool.isRequired,
status_key: PropTypes.string,
not_after: PropTypes.string,
warning_validation: PropTypes.string,
valid_chain: PropTypes.bool,
valid_key: PropTypes.bool,
valid_cert: PropTypes.bool,
valid_pair: PropTypes.bool,
dns_names: PropTypes.arrayOf(PropTypes.string),
key_type: PropTypes.string,
issuer: PropTypes.string,
subject: PropTypes.string,
t: PropTypes.func.isRequired,
setTlsConfig: PropTypes.func.isRequired,
validateTlsConfig: PropTypes.func.isRequired,
certificateSource: PropTypes.string,
privateKeySource: PropTypes.string,
privateKeySaved: PropTypes.bool,
};
const selector = formValueSelector(FORM_NAME.ENCRYPTION);
Form = connect((state) => {

View File

@@ -1,31 +1,27 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
const KeyStatus = ({ validKey, keyType }) => (
interface KeyStatusProps {
validKey: boolean;
keyType: string;
}
const KeyStatus = ({ validKey, keyType }: KeyStatusProps) => (
<Fragment>
<div className="form__label form__label--bold">
<Trans>encryption_status</Trans>:
</div>
<ul className="encryption__list">
<li className={validKey ? 'text-success' : 'text-danger'}>
{validKey ? (
<Trans values={{ type: keyType }}>
encryption_key_valid
</Trans>
<Trans values={{ type: keyType }}>encryption_key_valid</Trans>
) : (
<Trans values={{ type: keyType }}>
encryption_key_invalid
</Trans>
<Trans values={{ type: keyType }}>encryption_key_invalid</Trans>
)}
</li>
</ul>
</Fragment>
);
KeyStatus.propTypes = {
validKey: PropTypes.bool.isRequired,
keyType: PropTypes.string.isRequired,
};
export default withTranslation()(KeyStatus);

View File

@@ -1,15 +1,26 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import debounce from 'lodash/debounce';
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
import Form from './Form';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Encryption extends Component {
import Form from './Form';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import { EncryptionData } from '../../../initialState';
interface EncryptionProps {
setTlsConfig: (...args: unknown[]) => unknown;
validateTlsConfig: (...args: unknown[]) => unknown;
encryption: EncryptionData;
t: (...args: unknown[]) => string;
}
class Encryption extends Component<EncryptionProps> {
componentDidMount() {
const { validateTlsConfig, encryption } = this.props;
@@ -18,27 +29,24 @@ class Encryption extends Component {
}
}
handleFormSubmit = (values) => {
handleFormSubmit = (values: any) => {
const submitValues = this.getSubmitValues(values);
this.props.setTlsConfig(submitValues);
};
handleFormChange = debounce((values) => {
const submitValues = this.getSubmitValues(values);
if (submitValues.enabled || submitValues.serve_plain_dns) {
if (submitValues.enabled) {
this.props.validateTlsConfig(submitValues);
}
}, DEBOUNCE_TIMEOUT);
getInitialValues = (data) => {
getInitialValues = (data: any) => {
const { certificate_chain, private_key, private_key_saved } = data;
const certificate_source = certificate_chain
? ENCRYPTION_SOURCE.CONTENT
: ENCRYPTION_SOURCE.PATH;
const key_source = private_key || private_key_saved
? ENCRYPTION_SOURCE.CONTENT
: ENCRYPTION_SOURCE.PATH;
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
return {
...data,
@@ -47,10 +55,8 @@ class Encryption extends Component {
};
};
getSubmitValues = (values) => {
const {
certificate_source, key_source, private_key_saved, ...config
} = values;
getSubmitValues = (values: any) => {
const { certificate_source, key_source, private_key_saved, ...config } = values;
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
config.certificate_chain = '';
@@ -107,13 +113,13 @@ class Encryption extends Component {
return (
<div className="encryption">
<PageTitle title={t('encryption_settings')} />
{encryption.processing && <Loading />}
{!encryption.processing && (
<Card
title={t('encryption_title')}
subtitle={t('encryption_desc')}
bodyType="card-body box-body--settings"
>
bodyType="card-body box-body--settings">
<Form
initialValues={initialValues}
onSubmit={this.handleFormSubmit}
@@ -129,11 +135,4 @@ class Encryption extends Component {
}
}
Encryption.propTypes = {
setTlsConfig: PropTypes.func.isRequired,
validateTlsConfig: PropTypes.func.isRequired,
encryption: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(Encryption);

View File

@@ -1,17 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { CheckboxField, toNumber } from '../../../helpers/form';
import {
FILTERS_INTERVALS_HOURS,
FILTERS_RELATIVE_LINK,
FORM_NAME,
} from '../../../helpers/constants';
import { FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK, FORM_NAME } from '../../../helpers/constants';
const getTitleForInterval = (interval, t) => {
const getTitleForInterval = (interval: any, t: any) => {
if (interval === 0) {
return t('disabled');
}
@@ -22,15 +18,14 @@ const getTitleForInterval = (interval, t) => {
return t('interval_hours', { count: interval });
};
const getIntervalSelect = (processing, t, handleChange, toNumber) => (
const getIntervalSelect = (processing: any, t: any, handleChange: any, toNumber: any) => (
<Field
name="interval"
className="custom-select"
component="select"
onChange={handleChange}
normalize={toNumber}
disabled={processing}
>
disabled={processing}>
{FILTERS_INTERVALS_HOURS.map((interval) => (
<option value={interval} key={interval}>
{getTitleForInterval(interval, t)}
@@ -39,10 +34,18 @@ const getIntervalSelect = (processing, t, handleChange, toNumber) => (
</Field>
);
const Form = (props) => {
const {
handleSubmit, handleChange, processing, t,
} = props;
interface FormProps {
handleSubmit: (...args: unknown[]) => string;
handleChange?: (...args: unknown[]) => unknown;
change: (...args: unknown[]) => unknown;
submitting: boolean;
invalid: boolean;
processing: boolean;
t: (...args: unknown[]) => string;
}
const Form = (props: FormProps) => {
const { handleSubmit, handleChange, processing, t } = props;
const components = {
a: <a href={FILTERS_RELATIVE_LINK} rel="noopener noreferrer" />,
@@ -59,14 +62,13 @@ const Form = (props) => {
modifier="checkbox--settings"
component={CheckboxField}
placeholder={t('block_domain_use_filters_and_hosts')}
subtitle={<Trans components={components}>
filters_block_toggle_hint
</Trans>}
subtitle={<Trans components={components}>filters_block_toggle_hint</Trans>}
onChange={handleChange}
disabled={processing}
/>
</div>
</div>
<div className="col-12 col-md-5">
<div className="form__group form__group--inner mb-5">
<label className="form__label">
@@ -80,17 +82,4 @@ const Form = (props) => {
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default flow([
withTranslation(),
reduxForm({ form: FORM_NAME.FILTER_CONFIG }),
])(Form);
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.FILTER_CONFIG })])(Form);

View File

@@ -1,13 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import debounce from 'lodash/debounce';
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
import Form from './Form';
import { getObjDiff } from '../../../helpers/helpers';
const FiltersConfig = (props) => {
interface FiltersConfigProps {
initialValues: object;
processing: boolean;
setFiltersConfig: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
const FiltersConfig = (props: FiltersConfigProps) => {
const { initialValues, processing } = props;
const handleFormChange = debounce((values) => {
@@ -28,11 +36,4 @@ const FiltersConfig = (props) => {
);
};
FiltersConfig.propTypes = {
initialValues: PropTypes.object.isRequired,
processing: PropTypes.bool.isRequired,
setFiltersConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(FiltersConfig);

View File

@@ -1,11 +1,5 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
change,
Field,
formValueSelector,
reduxForm,
} from 'redux-form';
import { change, Field, formValueSelector, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
@@ -17,6 +11,7 @@ import {
renderInputField,
renderRadioField,
} from '../../../helpers/form';
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
import {
FORM_NAME,
@@ -30,7 +25,7 @@ import {
} from '../../../helpers/constants';
import '../FormButton.css';
const getIntervalTitle = (interval, t) => {
const getIntervalTitle = (interval: any, t: any) => {
switch (interval) {
case RETENTION_CUSTOM:
return t('settings_custom');
@@ -43,20 +38,34 @@ const getIntervalTitle = (interval, t) => {
}
};
const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.map((interval) => (
<Field
key={interval}
name="interval"
type="radio"
component={renderRadioField}
value={interval}
placeholder={getIntervalTitle(interval, t)}
normalize={toNumber}
disabled={processing}
/>
));
const getIntervalFields = (processing: any, t: any, toNumber: any) =>
QUERY_LOG_INTERVALS_DAYS.map((interval) => (
<Field
key={interval}
name="interval"
type="radio"
component={renderRadioField}
value={interval}
placeholder={getIntervalTitle(interval, t)}
normalize={toNumber}
disabled={processing}
/>
));
let Form = (props) => {
interface FormProps {
handleSubmit: (...args: unknown[]) => string;
handleClear: (...args: unknown[]) => unknown;
submitting: boolean;
invalid: boolean;
processing: boolean;
processingClear: boolean;
t: (...args: unknown[]) => string;
interval?: number;
customInterval?: number;
dispatch: (...args: unknown[]) => unknown;
}
let Form = (props: FormProps) => {
const {
handleSubmit,
submitting,
@@ -87,6 +96,7 @@ let Form = (props) => {
disabled={processing}
/>
</div>
<div className="form__group form__group--settings">
<Field
name="anonymize_client_ip"
@@ -97,9 +107,11 @@ let Form = (props) => {
disabled={processing}
/>
</div>
<label className="form__label">
<Trans>query_log_retention</Trans>
</label>
<div className="form__group form__group--settings">
<div className="custom-controls-stacked">
<Field
@@ -107,19 +119,15 @@ let Form = (props) => {
name="interval"
type="radio"
component={renderRadioField}
value={QUERY_LOG_INTERVALS_DAYS.includes(interval)
? RETENTION_CUSTOM
: interval
}
value={QUERY_LOG_INTERVALS_DAYS.includes(interval) ? RETENTION_CUSTOM : interval}
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
normalize={toFloatNumber}
disabled={processing}
/>
{!QUERY_LOG_INTERVALS_DAYS.includes(interval) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">
{t('custom_rotation_input')}
</div>
<div className="form__desc form__desc--top">{t('custom_rotation_input')}</div>
<Field
key={RETENTION_CUSTOM_INPUT}
name={CUSTOM_INTERVAL}
@@ -136,12 +144,15 @@ let Form = (props) => {
{getIntervalFields(processing, t, toFloatNumber)}
</div>
</div>
<label className="form__label form__label--with-desc">
<Trans>ignore_domains_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>ignore_domains_desc_query</Trans>
</div>
<div className="form__group form__group--settings">
<Field
name="ignored"
@@ -153,25 +164,25 @@ let Form = (props) => {
normalizeOnBlur={trimLinesAndRemoveEmpty}
/>
</div>
<div className="mt-5">
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={
submitting
|| invalid
|| processing
|| (!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
}
>
submitting ||
invalid ||
processing ||
(!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
}>
<Trans>save_btn</Trans>
</button>
<button
type="button"
className="btn btn-outline-secondary btn-standard form__button"
onClick={() => handleClear()}
disabled={processingClear}
>
disabled={processingClear}>
<Trans>query_log_clear</Trans>
</button>
</div>
@@ -179,19 +190,6 @@ let Form = (props) => {
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleClear: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
processingClear: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
interval: PropTypes.number,
customInterval: PropTypes.number,
dispatch: PropTypes.func.isRequired,
};
const selector = formValueSelector(FORM_NAME.LOG_CONFIG);
Form = connect((state) => {
@@ -203,7 +201,4 @@ Form = connect((state) => {
};
})(Form);
export default flow([
withTranslation(),
reduxForm({ form: FORM_NAME.LOG_CONFIG }),
])(Form);
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.LOG_CONFIG })])(Form);

View File

@@ -1,13 +1,26 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card';
import Form from './Form';
import { HOUR } from '../../../helpers/constants';
class LogsConfig extends Component {
handleFormSubmit = (values) => {
interface LogsConfigProps {
interval: number;
customInterval?: number;
enabled: boolean;
anonymize_client_ip: boolean;
processing: boolean;
ignored: unknown[];
processingClear: boolean;
setLogsConfig: (...args: unknown[]) => unknown;
clearLogs: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class LogsConfig extends Component<LogsConfigProps> {
handleFormSubmit = (values: any) => {
const { t, interval: prevInterval } = this.props;
const { interval, customInterval, ...rest } = values;
@@ -40,21 +53,24 @@ class LogsConfig extends Component {
render() {
const {
t,
enabled,
interval,
processing,
processingClear,
anonymize_client_ip,
ignored,
customInterval,
} = this.props;
return (
<Card
title={t('query_log_configuration')}
bodyType="card-body box-body--settings"
id="logs-config"
>
<Card title={t('query_log_configuration')} bodyType="card-body box-body--settings" id="logs-config">
<div className="form">
<Form
initialValues={{
@@ -75,17 +91,4 @@ class LogsConfig extends Component {
}
}
LogsConfig.propTypes = {
interval: PropTypes.number.isRequired,
customInterval: PropTypes.number,
enabled: PropTypes.bool.isRequired,
anonymize_client_ip: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
ignored: PropTypes.array.isRequired,
processingClear: PropTypes.bool.isRequired,
setLogsConfig: PropTypes.func.isRequired,
clearLogs: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(LogsConfig);

View File

@@ -80,11 +80,11 @@
}
.interface__ip:after {
content: ", ";
content: ', ';
}
.interface__ip:last-child:after {
content: "";
content: '';
}
.form__desc {
@@ -150,7 +150,9 @@
.custom-control-label,
.custom-control-label:before {
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
transition:
0.3s ease-in-out background-color,
0.3s ease-in-out color;
}
.custom-select:disabled {
@@ -158,5 +160,7 @@
}
.custom-select {
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
transition:
0.3s ease-in-out background-color,
0.3s ease-in-out color;
}

View File

@@ -1,8 +1,5 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
change, Field, formValueSelector, reduxForm,
} from 'redux-form';
import { change, Field, formValueSelector, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { connect } from 'react-redux';
@@ -24,10 +21,11 @@ import {
CUSTOM_INTERVAL,
RETENTION_RANGE,
} from '../../../helpers/constants';
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
import '../FormButton.css';
const getIntervalTitle = (intervalMs, t) => {
const getIntervalTitle = (intervalMs: any, t: any) => {
switch (intervalMs) {
case RETENTION_CUSTOM:
return t('settings_custom');
@@ -38,7 +36,21 @@ const getIntervalTitle = (intervalMs, t) => {
}
};
let Form = (props) => {
interface FormProps {
handleSubmit: (...args: unknown[]) => string;
handleReset: (...args: unknown[]) => string;
change: (...args: unknown[]) => unknown;
submitting: boolean;
invalid: boolean;
processing: boolean;
processingReset: boolean;
t: (...args: unknown[]) => string;
interval?: number;
customInterval?: number;
dispatch: (...args: unknown[]) => unknown;
}
let Form = (props: FormProps) => {
const {
handleSubmit,
processing,
@@ -69,12 +81,15 @@ let Form = (props) => {
disabled={processing}
/>
</div>
<label className="form__label form__label--with-desc">
<Trans>statistics_retention</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>statistics_retention_desc</Trans>
</div>
<div className="form__group form__group--settings mt-2">
<div className="custom-controls-stacked">
<Field
@@ -82,19 +97,15 @@ let Form = (props) => {
name="interval"
type="radio"
component={renderRadioField}
value={STATS_INTERVALS_DAYS.includes(interval)
? RETENTION_CUSTOM
: interval
}
value={STATS_INTERVALS_DAYS.includes(interval) ? RETENTION_CUSTOM : interval}
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
normalize={toFloatNumber}
disabled={processing}
/>
{!STATS_INTERVALS_DAYS.includes(interval) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">
{t('custom_retention_input')}
</div>
<div className="form__desc form__desc--top">{t('custom_retention_input')}</div>
<Field
key={RETENTION_CUSTOM_INPUT}
name={CUSTOM_INTERVAL}
@@ -122,12 +133,15 @@ let Form = (props) => {
))}
</div>
</div>
<label className="form__label form__label--with-desc">
<Trans>ignore_domains_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>ignore_domains_desc_stats</Trans>
</div>
<div className="form__group form__group--settings">
<Field
name="ignored"
@@ -139,25 +153,25 @@ let Form = (props) => {
normalizeOnBlur={trimLinesAndRemoveEmpty}
/>
</div>
<div className="mt-5">
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={
submitting
|| invalid
|| processing
|| (!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
}
>
submitting ||
invalid ||
processing ||
(!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
}>
<Trans>save_btn</Trans>
</button>
<button
type="button"
className="btn btn-outline-secondary btn-standard form__button"
onClick={() => handleReset()}
disabled={processingReset}
>
disabled={processingReset}>
<Trans>statistics_clear</Trans>
</button>
</div>
@@ -165,20 +179,6 @@ let Form = (props) => {
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
processingReset: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
interval: PropTypes.number,
customInterval: PropTypes.number,
dispatch: PropTypes.func.isRequired,
};
const selector = formValueSelector(FORM_NAME.STATS_CONFIG);
Form = connect((state) => {
@@ -190,7 +190,4 @@ Form = connect((state) => {
};
})(Form);
export default flow([
withTranslation(),
reduxForm({ form: FORM_NAME.STATS_CONFIG }),
])(Form);
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.STATS_CONFIG })])(Form);

View File

@@ -1,15 +1,25 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card';
import Form from './Form';
import { HOUR } from '../../../helpers/constants';
class StatsConfig extends Component {
handleFormSubmit = ({
enabled, interval, ignored, customInterval,
}) => {
interface StatsConfigProps {
interval: number;
customInterval?: number;
ignored: unknown[];
enabled: boolean;
processing: boolean;
processingReset: boolean;
setStatsConfig: (...args: unknown[]) => unknown;
resetStats: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class StatsConfig extends Component<StatsConfigProps> {
handleFormSubmit = ({ enabled, interval, ignored, customInterval }: any) => {
const { t, interval: prevInterval } = this.props;
const newInterval = customInterval ? customInterval * HOUR : interval;
@@ -39,20 +49,22 @@ class StatsConfig extends Component {
render() {
const {
t,
interval,
customInterval,
processing,
processingReset,
ignored,
enabled,
} = this.props;
return (
<Card
title={t('statistics_configuration')}
bodyType="card-body box-body--settings"
id="stats-config"
>
<Card title={t('statistics_configuration')} bodyType="card-body box-body--settings" id="stats-config">
<div className="form">
<Form
initialValues={{
@@ -72,16 +84,4 @@ class StatsConfig extends Component {
}
}
StatsConfig.propTypes = {
interval: PropTypes.number.isRequired,
customInterval: PropTypes.number,
ignored: PropTypes.array.isRequired,
enabled: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
processingReset: PropTypes.bool.isRequired,
setStatsConfig: PropTypes.func.isRequired,
resetStats: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(StatsConfig);

View File

@@ -1,17 +1,23 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import StatsConfig from './StatsConfig';
import LogsConfig from './LogsConfig';
import FiltersConfig from './FiltersConfig';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import { getObjectKeysSorted, captitalizeWords } from '../../helpers/helpers';
import './Settings.css';
import { SettingsData } from '../../initialState';
const ORDER_KEY = 'order';
@@ -30,47 +36,96 @@ const SETTINGS = {
},
};
class Settings extends Component {
interface SettingsProps {
initSettings: (...args: unknown[]) => unknown;
settings: SettingsData;
toggleSetting: (...args: unknown[]) => unknown;
getStatsConfig: (...args: unknown[]) => unknown;
setStatsConfig: (...args: unknown[]) => unknown;
resetStats: (...args: unknown[]) => unknown;
setFiltersConfig: (...args: unknown[]) => unknown;
getFilteringStatus: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
getLogsConfig?: (...args: unknown[]) => unknown;
setLogsConfig?: (...args: unknown[]) => unknown;
clearLogs?: (...args: unknown[]) => unknown;
stats?: {
processingGetConfig?: boolean;
interval?: number;
customInterval?: number;
enabled?: boolean;
ignored?: unknown[];
processingSetConfig?: boolean;
processingReset?: boolean;
};
queryLogs?: {
enabled?: boolean;
interval?: number;
customInterval?: number;
anonymize_client_ip?: boolean;
processingSetConfig?: boolean;
processingClear?: boolean;
processingGetConfig?: boolean;
ignored?: unknown[];
};
filtering?: {
interval?: number;
enabled?: boolean;
processingSetConfig?: boolean;
};
}
class Settings extends Component<SettingsProps> {
componentDidMount() {
this.props.initSettings(SETTINGS);
this.props.getStatsConfig();
this.props.getLogsConfig();
this.props.getFilteringStatus();
}
renderSettings = (settings) => getObjectKeysSorted(SETTINGS, ORDER_KEY)
.map((key) => {
renderSettings = (settings: any) =>
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
const setting = settings[key];
const { enabled } = setting;
return <Checkbox
{...setting}
key={key}
handleChange={() => this.props.toggleSetting(key, enabled)}
/>;
return <Checkbox {...setting} key={key} handleChange={() => this.props.toggleSetting(key, enabled)} />;
});
renderSafeSearch = () => {
const { settings: { settingsList: { safesearch } } } = this.props;
const {
settings: {
settingsList: { safesearch },
},
} = this.props;
const { enabled } = safesearch || {};
const searches = { ...(safesearch || {}) };
delete searches.enabled;
return (
<>
<Checkbox
enabled={enabled}
title='enforce_safe_search'
subtitle='enforce_save_search_hint'
handleChange={({ target: { checked: enabled } }) => this.props.toggleSetting('safesearch', { ...safesearch, enabled })}
title="enforce_safe_search"
subtitle="enforce_save_search_hint"
handleChange={({ target: { checked: enabled } }) =>
this.props.toggleSetting('safesearch', { ...safesearch, enabled })
}
/>
<div className='form__group--inner'>
<div className="form__group--inner">
{Object.keys(searches).map((searchKey) => (
<Checkbox
key={searchKey}
enabled={searches[searchKey]}
title={captitalizeWords(searchKey)}
subtitle=''
subtitle=""
disabled={!safesearch.enabled}
handleChange={({ target: { checked } }) => this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })}
handleChange={({ target: { checked } }: any) =>
this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })
}
/>
))}
</div>
@@ -81,24 +136,32 @@ class Settings extends Component {
render() {
const {
settings,
setStatsConfig,
resetStats,
stats,
queryLogs,
setLogsConfig,
clearLogs,
filtering,
setFiltersConfig,
t,
} = this.props;
const isDataReady = !settings.processing
&& !stats.processingGetConfig
&& !queryLogs.processingGetConfig;
const isDataReady = !settings.processing && !stats.processingGetConfig && !queryLogs.processingGetConfig;
return (
<Fragment>
<PageTitle title={t('general_settings')} />
{!isDataReady && <Loading />}
{isDataReady && (
<div className="content">
@@ -119,6 +182,7 @@ class Settings extends Component {
</div>
</Card>
</div>
<div className="col-md-12">
<LogsConfig
enabled={queryLogs.enabled}
@@ -132,6 +196,7 @@ class Settings extends Component {
clearLogs={clearLogs}
/>
</div>
<div className="col-md-12">
<StatsConfig
interval={stats.interval}
@@ -152,43 +217,4 @@ class Settings extends Component {
}
}
Settings.propTypes = {
initSettings: PropTypes.func.isRequired,
settings: PropTypes.object.isRequired,
toggleSetting: PropTypes.func.isRequired,
getStatsConfig: PropTypes.func.isRequired,
setStatsConfig: PropTypes.func.isRequired,
resetStats: PropTypes.func.isRequired,
setFiltersConfig: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
getLogsConfig: PropTypes.func,
setLogsConfig: PropTypes.func,
clearLogs: PropTypes.func,
stats: PropTypes.shape({
processingGetConfig: PropTypes.bool,
interval: PropTypes.number,
customInterval: PropTypes.number,
enabled: PropTypes.bool,
ignored: PropTypes.array,
processingSetConfig: PropTypes.bool,
processingReset: PropTypes.bool,
}),
queryLogs: PropTypes.shape({
enabled: PropTypes.bool,
interval: PropTypes.number,
customInterval: PropTypes.number,
anonymize_client_ip: PropTypes.bool,
processingSetConfig: PropTypes.bool,
processingClear: PropTypes.bool,
processingGetConfig: PropTypes.bool,
ignored: PropTypes.array,
}),
filtering: PropTypes.shape({
interval: PropTypes.number,
enabled: PropTypes.bool,
processingSetConfig: PropTypes.bool,
}),
};
export default withTranslation()(Settings);