all: sync with master
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
514
client/src/components/Settings/Clients/Form.tsx
Normal file
514
client/src/components/Settings/Clients/Form.tsx
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
166
client/src/components/Settings/Dhcp/FormDHCPv4.tsx
Normal file
166
client/src/components/Settings/Dhcp/FormDHCPv4.tsx
Normal 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);
|
||||
@@ -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);
|
||||
131
client/src/components/Settings/Dhcp/FormDHCPv6.tsx
Normal file
131
client/src/components/Settings/Dhcp/FormDHCPv6.tsx
Normal 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);
|
||||
@@ -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);
|
||||
118
client/src/components/Settings/Dhcp/Interfaces.tsx
Normal file
118
client/src/components/Settings/Dhcp/Interfaces.tsx
Normal 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);
|
||||
@@ -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),
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
321
client/src/components/Settings/Dhcp/index.tsx
Normal file
321
client/src/components/Settings/Dhcp/index.tsx
Normal 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;
|
||||
@@ -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);
|
||||
137
client/src/components/Settings/Dns/Access/Form.tsx
Normal file
137
client/src/components/Settings/Dns/Access/Form.tsx
Normal 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);
|
||||
@@ -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;
|
||||
27
client/src/components/Settings/Dns/Access/index.tsx
Normal file
27
client/src/components/Settings/Dns/Access/index.tsx
Normal 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;
|
||||
@@ -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);
|
||||
123
client/src/components/Settings/Dns/Cache/Form.tsx
Normal file
123
client/src/components/Settings/Dns/Cache/Form.tsx
Normal 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);
|
||||
@@ -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={{
|
||||
@@ -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);
|
||||
310
client/src/components/Settings/Dns/Config/Form.tsx
Normal file
310
client/src/components/Settings/Dns/Config/Form.tsx
Normal 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);
|
||||
@@ -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={{
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
338
client/src/components/Settings/Dns/Upstream/Form.tsx
Normal file
338
client/src/components/Settings/Dns/Upstream/Form.tsx
Normal 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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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) => {
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user