fix forms
This commit is contained in:
@@ -1,47 +1,67 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import i18next from 'i18next';
|
||||||
import { CLIENT_ID_LINK } from '../../../../helpers/constants';
|
import { CLIENT_ID_LINK } from '../../../../helpers/constants';
|
||||||
import { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers';
|
import { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers';
|
||||||
import { Textarea } from '../../../ui/Controls/Textarea';
|
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||||
|
|
||||||
const fields = [
|
type FormData = {
|
||||||
|
allowed_clients: string;
|
||||||
|
disallowed_clients: string;
|
||||||
|
blocked_hosts: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields: {
|
||||||
|
id: keyof FormData;
|
||||||
|
title: string;
|
||||||
|
subtitle: ReactNode;
|
||||||
|
normalizeOnBlur: (value: string) => string;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
id: 'allowed_clients',
|
id: 'allowed_clients',
|
||||||
title: 'access_allowed_title',
|
title: i18next.t('access_allowed_title'),
|
||||||
subtitle: 'access_allowed_desc',
|
subtitle: (
|
||||||
|
<Trans
|
||||||
|
components={{
|
||||||
|
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||||
|
}}>
|
||||||
|
access_allowed_desc
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
normalizeOnBlur: removeEmptyLines,
|
normalizeOnBlur: removeEmptyLines,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'disallowed_clients',
|
id: 'disallowed_clients',
|
||||||
title: 'access_disallowed_title',
|
title: i18next.t('access_disallowed_title'),
|
||||||
subtitle: 'access_disallowed_desc',
|
subtitle: (
|
||||||
|
<Trans
|
||||||
|
components={{
|
||||||
|
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||||
|
}}>
|
||||||
|
access_disallowed_desc
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
normalizeOnBlur: trimMultilineString,
|
normalizeOnBlur: trimMultilineString,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'blocked_hosts',
|
id: 'blocked_hosts',
|
||||||
title: 'access_blocked_title',
|
title: i18next.t('access_blocked_title'),
|
||||||
subtitle: 'access_blocked_desc',
|
subtitle: i18next.t('access_blocked_desc'),
|
||||||
normalizeOnBlur: removeEmptyLines,
|
normalizeOnBlur: removeEmptyLines,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface FormProps {
|
type FormProps = {
|
||||||
initialValues?: {
|
initialValues?: {
|
||||||
allowed_clients?: string;
|
allowed_clients?: string;
|
||||||
disallowed_clients?: string;
|
disallowed_clients?: string;
|
||||||
blocked_hosts?: string;
|
blocked_hosts?: string;
|
||||||
};
|
};
|
||||||
onSubmit: (data: any) => void;
|
onSubmit: (data: FormData) => void;
|
||||||
processingSet: boolean;
|
processingSet: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
allowed_clients: string;
|
|
||||||
disallowed_clients: string;
|
|
||||||
blocked_hosts: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -70,7 +90,7 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
|||||||
}: {
|
}: {
|
||||||
id: keyof FormData;
|
id: keyof FormData;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: ReactNode;
|
||||||
normalizeOnBlur: (value: string) => string;
|
normalizeOnBlur: (value: string) => string;
|
||||||
}) => {
|
}) => {
|
||||||
const disabled = allowedClients && id === 'disallowed_clients';
|
const disabled = allowedClients && id === 'disallowed_clients';
|
||||||
@@ -78,22 +98,11 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
|||||||
return (
|
return (
|
||||||
<div key={id} className="form__group mb-5">
|
<div key={id} className="form__group mb-5">
|
||||||
<label className="form__label form__label--with-desc" htmlFor={id}>
|
<label className="form__label form__label--with-desc" htmlFor={id}>
|
||||||
{t(title)}
|
{title}
|
||||||
{disabled && (
|
{disabled && <> ({t('disabled')})</>}
|
||||||
<>
|
|
||||||
<span> </span>({t('disabled')})
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">{subtitle}</div>
|
||||||
<Trans
|
|
||||||
components={{
|
|
||||||
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
|
||||||
}}>
|
|
||||||
{subtitle}
|
|
||||||
</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name={id}
|
name={id}
|
||||||
@@ -102,6 +111,7 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
|||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
id={id}
|
id={id}
|
||||||
|
data-testid={id}
|
||||||
disabled={disabled || processingSet}
|
disabled={disabled || processingSet}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
field.onChange(normalizeOnBlur(e.target.value));
|
field.onChange(normalizeOnBlur(e.target.value));
|
||||||
@@ -115,21 +125,13 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{fields.map((f) =>
|
{fields.map((f) => renderField(f))}
|
||||||
renderField(
|
|
||||||
f as {
|
|
||||||
id: keyof FormData;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
normalizeOnBlur: (value: string) => string;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="access_save"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={isSubmitting || !isDirty || processingSet}>
|
disabled={isSubmitting || !isDirty || processingSet}>
|
||||||
{t('save_config')}
|
{t('save_config')}
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import i18next from 'i18next';
|
||||||
import { clearDnsCache } from '../../../../actions/dnsConfig';
|
import { clearDnsCache } from '../../../../actions/dnsConfig';
|
||||||
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from '../../../../helpers/constants';
|
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from '../../../../helpers/constants';
|
||||||
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
|
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
|
||||||
import { RootState } from '../../../../initialState';
|
import { RootState } from '../../../../initialState';
|
||||||
|
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||||
|
|
||||||
const INPUTS_FIELDS = [
|
const INPUTS_FIELDS = [
|
||||||
{
|
{
|
||||||
name: CACHE_CONFIG_FIELDS.cache_size,
|
name: CACHE_CONFIG_FIELDS.cache_size,
|
||||||
title: 'cache_size',
|
title: i18next.t('cache_size'),
|
||||||
description: 'cache_size_desc',
|
description: i18next.t('cache_size_desc'),
|
||||||
placeholder: 'enter_cache_size',
|
placeholder: i18next.t('enter_cache_size'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
|
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
|
||||||
title: 'cache_ttl_min_override',
|
title: i18next.t('cache_ttl_min_override'),
|
||||||
description: 'cache_ttl_min_override_desc',
|
description: i18next.t('cache_ttl_min_override_desc'),
|
||||||
placeholder: 'enter_cache_ttl_min_override',
|
placeholder: i18next.t('enter_cache_ttl_min_override'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
|
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
|
||||||
title: 'cache_ttl_max_override',
|
title: i18next.t('cache_ttl_max_override'),
|
||||||
description: 'cache_ttl_max_override_desc',
|
description: i18next.t('cache_ttl_max_override_desc'),
|
||||||
placeholder: 'enter_cache_ttl_max_override',
|
placeholder: i18next.t('enter_cache_ttl_max_override'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
|
control,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
@@ -81,15 +84,16 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
|
|||||||
<div className="col-12 col-md-7 p-0">
|
<div className="col-12 col-md-7 p-0">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor={name} className="form__label form__label--with-desc">
|
<label htmlFor={name} className="form__label form__label--with-desc">
|
||||||
{t(title)}
|
{title}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">{t(description)}</div>
|
<div className="form__desc form__desc--top">{description}</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
data-testid={`dns_${name}`}
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(placeholder)}
|
placeholder={placeholder}
|
||||||
disabled={processingSetConfig}
|
disabled={processingSetConfig}
|
||||||
min={0}
|
min={0}
|
||||||
max={UINT32_RANGE.MAX}
|
max={UINT32_RANGE.MAX}
|
||||||
@@ -108,26 +112,26 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="checkbox">
|
<Controller
|
||||||
<input
|
name="cache_optimistic"
|
||||||
type="checkbox"
|
control={control}
|
||||||
className="checkbox__input"
|
render={({ field }) => (
|
||||||
disabled={processingSetConfig}
|
<Checkbox
|
||||||
{...register('cache_optimistic')}
|
{...field}
|
||||||
/>
|
data-testid="dns_cache_optimistic"
|
||||||
<span className="checkbox__label">
|
title={t('cache_optimistic')}
|
||||||
<span className="checkbox__label-text checkbox__label-text--long">
|
subtitle={t('cache_optimistic_desc')}
|
||||||
<span className="checkbox__label-title">{t('cache_optimistic')}</span>
|
disabled={processingSetConfig}
|
||||||
<span className="checkbox__label-subtitle">{t('cache_optimistic_desc')}</span>
|
/>
|
||||||
</span>
|
)}
|
||||||
</span>
|
/>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="dns_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={isSubmitting || !isDirty || processingSetConfig || minExceedsMax}>
|
disabled={isSubmitting || !isDirty || processingSetConfig || minExceedsMax}>
|
||||||
{t('save_btn')}
|
{t('save_btn')}
|
||||||
@@ -135,6 +139,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="dns_clear"
|
||||||
className="btn btn-outline-secondary btn-standard form__button"
|
className="btn btn-outline-secondary btn-standard form__button"
|
||||||
onClick={handleClearCache}>
|
onClick={handleClearCache}>
|
||||||
{t('clear_cache')}
|
{t('clear_cache')}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import i18next from 'i18next';
|
|||||||
import { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from '../../../../helpers/validators';
|
import { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from '../../../../helpers/validators';
|
||||||
|
|
||||||
import { BLOCKING_MODES, UINT32_RANGE } from '../../../../helpers/constants';
|
import { BLOCKING_MODES, UINT32_RANGE } from '../../../../helpers/constants';
|
||||||
import { removeEmptyLines } from '../../../../helpers/helpers';
|
|
||||||
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||||
|
import { Input } from '../../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../../helpers/form';
|
||||||
|
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||||
|
import { Radio } from '../../../ui/Controls/Radio';
|
||||||
|
|
||||||
const checkboxes: {
|
const checkboxes: {
|
||||||
name: 'dnssec_enabled' | 'disable_ipv6';
|
name: 'dnssec_enabled' | 'disable_ipv6';
|
||||||
@@ -26,19 +29,57 @@ const checkboxes: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const customIps = [
|
const customIps: {
|
||||||
|
name: 'blocking_ipv4' | 'blocking_ipv6';
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
validateIp: (value: string) => string;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
description: 'blocking_ipv4_desc',
|
|
||||||
name: 'blocking_ipv4',
|
name: 'blocking_ipv4',
|
||||||
|
label: i18next.t('blocking_ipv4'),
|
||||||
|
description: i18next.t('blocking_ipv4_desc'),
|
||||||
validateIp: validateIpv4,
|
validateIp: validateIpv4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'blocking_ipv6_desc',
|
|
||||||
name: 'blocking_ipv6',
|
name: 'blocking_ipv6',
|
||||||
|
label: i18next.t('blocking_ipv6'),
|
||||||
|
description: i18next.t('blocking_ipv6_desc'),
|
||||||
validateIp: validateIpv6,
|
validateIp: validateIpv6,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const blockingModeOptions = [
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.default,
|
||||||
|
label: i18next.t('default'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.refused,
|
||||||
|
label: i18next.t('refused'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.nxdomain,
|
||||||
|
label: i18next.t('nxdomain'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.null_ip,
|
||||||
|
label: i18next.t('null_ip'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.custom_ip,
|
||||||
|
label: i18next.t('custom_ip'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const blockingModeDescriptions = [
|
||||||
|
i18next.t(`blocking_mode_default`),
|
||||||
|
i18next.t(`blocking_mode_refused`),
|
||||||
|
i18next.t(`blocking_mode_nxdomain`),
|
||||||
|
i18next.t(`blocking_mode_null_ip`),
|
||||||
|
i18next.t(`blocking_mode_custom_ip`),
|
||||||
|
];
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
ratelimit: number;
|
ratelimit: number;
|
||||||
ratelimit_subnet_len_ipv4: number;
|
ratelimit_subnet_len_ipv4: number;
|
||||||
@@ -46,7 +87,7 @@ type FormData = {
|
|||||||
ratelimit_whitelist: string;
|
ratelimit_whitelist: string;
|
||||||
edns_cs_enabled: boolean;
|
edns_cs_enabled: boolean;
|
||||||
edns_cs_use_custom: boolean;
|
edns_cs_use_custom: boolean;
|
||||||
edns_cs_custom_ip?: boolean;
|
edns_cs_custom_ip?: string;
|
||||||
dnssec_enabled: boolean;
|
dnssec_enabled: boolean;
|
||||||
disable_ipv6: boolean;
|
disable_ipv6: boolean;
|
||||||
blocking_mode: string;
|
blocking_mode: string;
|
||||||
@@ -65,11 +106,10 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
@@ -84,107 +124,102 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
|
<Controller
|
||||||
{t('rate_limit')}
|
name="ratelimit"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
<div className="form__desc form__desc--top">{t('rate_limit_desc')}</div>
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
<input
|
{...field}
|
||||||
id="ratelimit"
|
data-testid="dns_config_ratelimit"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
label={t('rate_limit')}
|
||||||
disabled={processing}
|
desc={t('rate_limit_desc')}
|
||||||
{...register('ratelimit', {
|
error={fieldState.error?.message}
|
||||||
required: t('form_error_required'),
|
min={UINT32_RANGE.MIN}
|
||||||
valueAsNumber: true,
|
max={UINT32_RANGE.MAX}
|
||||||
min: UINT32_RANGE.MIN,
|
disabled={processing}
|
||||||
max: UINT32_RANGE.MAX,
|
onChange={(e) => {
|
||||||
})}
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.ratelimit && (
|
|
||||||
<div className="form__message form__message--error">{errors.ratelimit.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit_subnet_len_ipv4" className="form__label form__label--with-desc">
|
<Controller
|
||||||
{t('rate_limit_subnet_len_ipv4')}
|
name="ratelimit_subnet_len_ipv4"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
<div className="form__desc form__desc--top">{t('rate_limit_subnet_len_ipv4_desc')}</div>
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
<input
|
{...field}
|
||||||
id="ratelimit_subnet_len_ipv4"
|
data-testid="dns_config_subnet_ipv4"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
label={t('rate_limit_subnet_len_ipv4')}
|
||||||
disabled={processing}
|
desc={t('rate_limit_subnet_len_ipv4_desc')}
|
||||||
{...register('ratelimit_subnet_len_ipv4', {
|
error={fieldState.error?.message}
|
||||||
required: t('form_error_required'),
|
min={0}
|
||||||
valueAsNumber: true,
|
max={32}
|
||||||
min: 0,
|
disabled={processing}
|
||||||
max: 32,
|
onChange={(e) => {
|
||||||
})}
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.ratelimit_subnet_len_ipv4 && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.ratelimit_subnet_len_ipv4.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit_subnet_len_ipv6" className="form__label form__label--with-desc">
|
<Controller
|
||||||
{t('rate_limit_subnet_len_ipv6')}
|
name="ratelimit_subnet_len_ipv6"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
<div className="form__desc form__desc--top">{t('rate_limit_subnet_len_ipv6_desc')}</div>
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
<input
|
{...field}
|
||||||
id="ratelimit_subnet_len_ipv6"
|
data-testid="dns_config_subnet_ipv6"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
label={t('rate_limit_subnet_len_ipv6')}
|
||||||
disabled={processing}
|
desc={t('rate_limit_subnet_len_ipv6_desc')}
|
||||||
{...register('ratelimit_subnet_len_ipv6', {
|
error={fieldState.error?.message}
|
||||||
required: t('form_error_required'),
|
min={0}
|
||||||
valueAsNumber: true,
|
max={128}
|
||||||
min: 0,
|
disabled={processing}
|
||||||
max: 128,
|
onChange={(e) => {
|
||||||
})}
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.ratelimit_subnet_len_ipv6 && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.ratelimit_subnet_len_ipv6.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit_whitelist" className="form__label form__label--with-desc">
|
<Controller
|
||||||
{t('rate_limit_whitelist')}
|
name="ratelimit_whitelist"
|
||||||
</label>
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
<div className="form__desc form__desc--top">{t('rate_limit_whitelist_desc')}</div>
|
<Textarea
|
||||||
|
{...field}
|
||||||
<textarea
|
data-testid="dns_config_subnet_ipv6"
|
||||||
id="ratelimit_whitelist"
|
label={t('rate_limit_whitelist')}
|
||||||
className="form-control"
|
desc={t('rate_limit_whitelist_desc')}
|
||||||
disabled={processing}
|
error={fieldState.error?.message}
|
||||||
{...register('ratelimit_whitelist', {
|
disabled={processing}
|
||||||
onChange: removeEmptyLines,
|
trimOnBlur
|
||||||
})}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.ratelimit_whitelist && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.ratelimit_whitelist.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -194,7 +229,12 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
name="edns_cs_enabled"
|
name="edns_cs_enabled"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox {...field} title={t('edns_enable')} disabled={processing} />
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid="dns_config_edns_cs_enabled"
|
||||||
|
title={t('edns_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,6 +248,7 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="dns_config_edns_use_custom_ip"
|
||||||
title={t('edns_use_custom_ip')}
|
title={t('edns_use_custom_ip')}
|
||||||
disabled={processing || !edns_cs_enabled}
|
disabled={processing || !edns_cs_enabled}
|
||||||
/>
|
/>
|
||||||
@@ -216,15 +257,23 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{edns_cs_use_custom && (
|
{edns_cs_use_custom && (
|
||||||
<input
|
<Controller
|
||||||
id="edns_cs_custom_ip"
|
name="edns_cs_custom_ip"
|
||||||
type="text"
|
control={control}
|
||||||
className="form-control"
|
rules={{
|
||||||
disabled={processing || !edns_cs_enabled}
|
validate: {
|
||||||
{...register('edns_cs_custom_ip', {
|
required: validateRequiredValue,
|
||||||
required: t('form_error_required'),
|
id: validateIp,
|
||||||
validate: (value) => validateIp(value) || validateRequiredValue(value),
|
},
|
||||||
})}
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
data-testid="dns_config_edns_cs_custom_ip"
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={processing || !edns_cs_enabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -238,6 +287,7 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid={`dns_config_${name}`}
|
||||||
title={placeholder}
|
title={placeholder}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@@ -253,53 +303,52 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
<label className="form__label form__label--with-desc">{t('blocking_mode')}</label>
|
<label className="form__label form__label--with-desc">{t('blocking_mode')}</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
{Object.values(BLOCKING_MODES).map((mode: any) => (
|
{blockingModeDescriptions.map((desc: string) => (
|
||||||
<li key={mode}>{t(`blocking_mode_${mode}`)}</li>
|
<li key={desc}>{desc}</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
{Object.values(BLOCKING_MODES).map((mode: any) => (
|
<Controller
|
||||||
<label key={mode} className="custom-control custom-radio">
|
name="blocking_mode"
|
||||||
<input
|
control={control}
|
||||||
type="radio"
|
render={({ field }) => (
|
||||||
className="custom-control-input"
|
<Radio {...field} options={blockingModeOptions} disabled={processing} />
|
||||||
value={mode}
|
)}
|
||||||
disabled={processing}
|
/>
|
||||||
{...register('blocking_mode')}
|
|
||||||
/>
|
|
||||||
<span className="custom-control-label">{t(mode)}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{blocking_mode === BLOCKING_MODES.custom_ip && (
|
{blocking_mode === BLOCKING_MODES.custom_ip && (
|
||||||
<>
|
<>
|
||||||
{customIps.map(({ description, name, validateIp }) => (
|
{customIps.map(({ label, description, name, validateIp }) => (
|
||||||
<div className="col-12 col-sm-6" key={name}>
|
<div className="col-12 col-sm-6" key={name}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="form__label form__label--with-desc" htmlFor={name}>
|
<Controller
|
||||||
{t(name)}
|
name={name}
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{
|
||||||
<div className="form__desc form__desc--top">{t(description)}</div>
|
validate: {
|
||||||
|
required: validateRequiredValue,
|
||||||
<input
|
ip: validateIp,
|
||||||
id={name}
|
},
|
||||||
type="text"
|
}}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
disabled={processing}
|
<Input
|
||||||
{...register(name as keyof FormData, {
|
{...field}
|
||||||
required: t('form_error_required'),
|
data-testid="dns_config_blocked_response_ttl"
|
||||||
validate: (value) => validateIp(value) || validateRequiredValue(value),
|
type="number"
|
||||||
})}
|
label={label}
|
||||||
|
desc={description}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={processing}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors[name as keyof FormData] && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors[name as keyof FormData]?.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -308,35 +357,35 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="blocked_response_ttl" className="form__label form__label--with-desc">
|
<Controller
|
||||||
{t('blocked_response_ttl')}
|
name="blocked_response_ttl"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
<div className="form__desc form__desc--top">{t('blocked_response_ttl_desc')}</div>
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
<input
|
{...field}
|
||||||
id="blocked_response_ttl"
|
data-testid="dns_config_blocked_response_ttl"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
label={t('blocked_response_ttl')}
|
||||||
disabled={processing}
|
desc={t('blocked_response_ttl_desc')}
|
||||||
{...register('blocked_response_ttl', {
|
error={fieldState.error?.message}
|
||||||
required: t('form_error_required'),
|
min={UINT32_RANGE.MIN}
|
||||||
valueAsNumber: true,
|
max={UINT32_RANGE.MAX}
|
||||||
min: UINT32_RANGE.MIN,
|
disabled={processing}
|
||||||
max: UINT32_RANGE.MAX,
|
onChange={(e) => {
|
||||||
})}
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.blocked_response_ttl && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.blocked_response_ttl.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="dns_config_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={isSubmitting || !isDirty || processing}>
|
disabled={isSubmitting || !isDirty || processing}>
|
||||||
{t('save_btn')}
|
{t('save_btn')}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import classnames from 'classnames';
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import i18next from 'i18next';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { testUpstreamWithFormValues } from '../../../../actions';
|
import { testUpstreamWithFormValues } from '../../../../actions';
|
||||||
import { DNS_REQUEST_OPTIONS, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';
|
import { DNS_REQUEST_OPTIONS, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';
|
||||||
import { removeEmptyLines } from '../../../../helpers/helpers';
|
import { removeEmptyLines } from '../../../../helpers/helpers';
|
||||||
@@ -12,9 +13,10 @@ import { RootState } from '../../../../initialState';
|
|||||||
import '../../../ui/texareaCommentsHighlight.css';
|
import '../../../ui/texareaCommentsHighlight.css';
|
||||||
import Examples from './Examples';
|
import Examples from './Examples';
|
||||||
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||||
|
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||||
|
import { Radio } from '../../../ui/Controls/Radio';
|
||||||
|
|
||||||
const UPSTREAM_DNS_NAME = 'upstream_dns';
|
const UPSTREAM_DNS_NAME = 'upstream_dns';
|
||||||
const UPSTREAM_MODE_NAME = 'upstream_mode';
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
upstream_dns: string;
|
upstream_dns: string;
|
||||||
@@ -31,24 +33,21 @@ type FormProps = {
|
|||||||
onSubmit: (data: FormData) => void;
|
onSubmit: (data: FormData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INPUT_FIELDS = [
|
const upstreamModeOptions = [
|
||||||
{
|
{
|
||||||
name: UPSTREAM_MODE_NAME,
|
label: i18next.t('load_balancing'),
|
||||||
|
desc: i18next.t('load_balancing_desc'),
|
||||||
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||||
subtitle: 'load_balancing_desc',
|
|
||||||
placeholder: 'load_balancing',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: UPSTREAM_MODE_NAME,
|
label: i18next.t('parallel_requests'),
|
||||||
|
desc: i18next.t('upstream_parallel'),
|
||||||
value: DNS_REQUEST_OPTIONS.PARALLEL,
|
value: DNS_REQUEST_OPTIONS.PARALLEL,
|
||||||
subtitle: 'upstream_parallel',
|
|
||||||
placeholder: 'parallel_requests',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: UPSTREAM_MODE_NAME,
|
label: i18next.t('fastest_addr'),
|
||||||
|
desc: i18next.t('fastest_addr_desc'),
|
||||||
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
|
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
|
||||||
subtitle: 'fastest_addr_desc',
|
|
||||||
placeholder: 'fastest_addr',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -91,14 +90,10 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
dispatch(testUpstreamWithFormValues(formValues));
|
dispatch(testUpstreamWithFormValues(formValues));
|
||||||
};
|
};
|
||||||
|
|
||||||
const testButtonClass = classnames('btn btn-primary btn-standard mr-2', {
|
|
||||||
'btn-loading': processingTestUpstream,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="form--upstream">
|
<form onSubmit={handleSubmit(onSubmit)} className="form--upstream">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<label className="col form__label" htmlFor={UPSTREAM_DNS_NAME}>
|
<label className="col form__label" htmlFor="upstream_dns">
|
||||||
<Trans
|
<Trans
|
||||||
components={{
|
components={{
|
||||||
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
|
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||||
@@ -126,17 +121,16 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
<textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
id={UPSTREAM_DNS_NAME}
|
id={UPSTREAM_DNS_NAME}
|
||||||
className="form-control form-control--textarea font-monospace text-input"
|
data-testid="upstream_dns"
|
||||||
|
className="form-control--textarea-large text-input"
|
||||||
|
wrapperClassName="mb-0"
|
||||||
placeholder={t('upstream_dns')}
|
placeholder={t('upstream_dns')}
|
||||||
disabled={!!upstream_dns_file || processingSetConfig || processingTestUpstream}
|
disabled={!!upstream_dns_file || processingSetConfig || processingTestUpstream}
|
||||||
onScroll={(e) => syncScroll(e, textareaRef)}
|
onScroll={(e) => syncScroll(e, textareaRef)}
|
||||||
onBlur={(e) => {
|
trimOnBlur
|
||||||
const value = removeEmptyLines(e.target.value);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{getTextareaCommentsHighlight(textareaRef, upstream_dns)}
|
{getTextareaCommentsHighlight(textareaRef, upstream_dns)}
|
||||||
</>
|
</>
|
||||||
@@ -150,31 +144,19 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{INPUT_FIELDS.map(({ name, value, subtitle, placeholder }) => (
|
<div className="col-12 mb-4">
|
||||||
<div key={value} className="col-12 mb-4">
|
<Controller
|
||||||
<Controller
|
name="upstream_mode"
|
||||||
name="upstream_mode"
|
control={control}
|
||||||
control={control}
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<Radio
|
||||||
<div className="custom-control custom-radio">
|
{...field}
|
||||||
<input
|
options={upstreamModeOptions}
|
||||||
{...field}
|
disabled={processingSetConfig || processingTestUpstream}
|
||||||
type="radio"
|
/>
|
||||||
className="custom-control-input"
|
)}
|
||||||
id={`${name}_${value}`}
|
/>
|
||||||
value={value}
|
</div>
|
||||||
checked={field.value === value}
|
|
||||||
disabled={processingSetConfig || processingTestUpstream}
|
|
||||||
/>
|
|
||||||
<label className="custom-control-label" htmlFor={`${name}_${value}`}>
|
|
||||||
<span className="custom-control-label__title">{t(placeholder)}</span>
|
|
||||||
<span className="custom-control-label__subtitle">{t(subtitle)}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
|
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
|
||||||
@@ -187,16 +169,14 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
name="fallback_dns"
|
name="fallback_dns"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
id="fallback_dns"
|
id="fallback_dns"
|
||||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
data-testid="fallback_dns"
|
||||||
|
wrapperClassName="mb-0"
|
||||||
placeholder={t('fallback_dns_placeholder')}
|
placeholder={t('fallback_dns_placeholder')}
|
||||||
disabled={processingSetConfig}
|
disabled={processingSetConfig}
|
||||||
onBlur={(e) => {
|
trimOnBlur
|
||||||
const value = removeEmptyLines(e.target.value);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -206,7 +186,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 mb-2">
|
<div className="col-12">
|
||||||
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
|
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
|
||||||
{t('bootstrap_dns')}
|
{t('bootstrap_dns')}
|
||||||
</label>
|
</label>
|
||||||
@@ -217,11 +197,12 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
name="bootstrap_dns"
|
name="bootstrap_dns"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
id="bootstrap_dns"
|
id="bootstrap_dns"
|
||||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
data-testid="bootstrap_dns"
|
||||||
placeholder={t('bootstrap_dns')}
|
placeholder={t('bootstrap_dns')}
|
||||||
|
wrapperClassName="mb-0"
|
||||||
disabled={processingSetConfig}
|
disabled={processingSetConfig}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const value = removeEmptyLines(e.target.value);
|
const value = removeEmptyLines(e.target.value);
|
||||||
@@ -255,16 +236,13 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
name="local_ptr_upstreams"
|
name="local_ptr_upstreams"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
id="local_ptr_upstreams"
|
id="local_ptr_upstreams"
|
||||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
data-testid="local_ptr_upstreams"
|
||||||
placeholder={t('local_ptr_placeholder')}
|
placeholder={t('local_ptr_placeholder')}
|
||||||
disabled={processingSetConfig}
|
disabled={processingSetConfig}
|
||||||
onBlur={(e) => {
|
trimOnBlur
|
||||||
const value = removeEmptyLines(e.target.value);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -276,6 +254,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="dns_use_private_ptr_resolvers"
|
||||||
title={t('use_private_ptr_resolvers_title')}
|
title={t('use_private_ptr_resolvers_title')}
|
||||||
subtitle={t('use_private_ptr_resolvers_desc')}
|
subtitle={t('use_private_ptr_resolvers_desc')}
|
||||||
disabled={processingSetConfig}
|
disabled={processingSetConfig}
|
||||||
@@ -296,6 +275,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="dns_resolve_clients"
|
||||||
title={t('resolve_clients_title')}
|
title={t('resolve_clients_title')}
|
||||||
subtitle={t('resolve_clients_desc')}
|
subtitle={t('resolve_clients_desc')}
|
||||||
disabled={processingSetConfig}
|
disabled={processingSetConfig}
|
||||||
@@ -309,7 +289,10 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={testButtonClass}
|
data-testid="dns_upstream_test"
|
||||||
|
className={clsx('btn btn-primary btn-standard mr-2', {
|
||||||
|
'btn-loading': processingTestUpstream,
|
||||||
|
})}
|
||||||
onClick={handleUpstreamTest}
|
onClick={handleUpstreamTest}
|
||||||
disabled={!upstream_dns || processingTestUpstream}>
|
disabled={!upstream_dns || processingTestUpstream}>
|
||||||
{t('test_upstream_btn')}
|
{t('test_upstream_btn')}
|
||||||
@@ -317,6 +300,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="dns_upstream_save"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={isSubmitting || !isDirty || processingSetConfig || processingTestUpstream}>
|
disabled={isSubmitting || !isDirty || processingSetConfig || processingTestUpstream}>
|
||||||
{t('apply_btn')}
|
{t('apply_btn')}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import i18next from 'i18next';
|
|||||||
import { toNumber } from '../../../helpers/form';
|
import { toNumber } from '../../../helpers/form';
|
||||||
import { DAY, FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK } from '../../../helpers/constants';
|
import { DAY, FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK } from '../../../helpers/constants';
|
||||||
import { Checkbox } from '../../ui/Controls/Checkbox';
|
import { Checkbox } from '../../ui/Controls/Checkbox';
|
||||||
|
import { Select } from '../../ui/Controls/Select';
|
||||||
|
|
||||||
const THREE_DAYS_INTERVAL = DAY * 3;
|
const THREE_DAYS_INTERVAL = DAY * 3;
|
||||||
const SEVEN_DAYS_INTERVAL = DAY * 7;
|
const SEVEN_DAYS_INTERVAL = DAY * 7;
|
||||||
@@ -37,7 +38,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const prevFormValuesRef = useRef<FormValues>(initialValues);
|
const prevFormValuesRef = useRef<FormValues>(initialValues);
|
||||||
|
|
||||||
const { register, watch, control } = useForm({
|
const { watch, control } = useForm({
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
});
|
});
|
||||||
@@ -68,6 +69,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="filters_enabled"
|
||||||
title={t('block_domain_use_filters_and_hosts')}
|
title={t('block_domain_use_filters_and_hosts')}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
/>
|
/>
|
||||||
@@ -85,18 +87,26 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
|
|||||||
<label className="form__label">
|
<label className="form__label">
|
||||||
<Trans>filters_interval</Trans>
|
<Trans>filters_interval</Trans>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Controller
|
||||||
{...register('interval', {
|
name="interval"
|
||||||
setValueAs: toNumber,
|
control={control}
|
||||||
})}
|
render={({ field }) => (
|
||||||
className="custom-select"
|
<Select
|
||||||
disabled={processing}>
|
{...field}
|
||||||
{FILTERS_INTERVALS_HOURS.map((interval) => (
|
data-testid="filters_interval"
|
||||||
<option value={interval} key={interval}>
|
disabled={processing}
|
||||||
{getTitleForInterval(interval)}
|
onChange={(e) => {
|
||||||
</option>
|
const { value } = e.target;
|
||||||
))}
|
field.onChange(toNumber(value));
|
||||||
</select>
|
}}>
|
||||||
|
{FILTERS_INTERVALS_HOURS.map((interval) => (
|
||||||
|
<option value={interval} key={interval}>
|
||||||
|
{getTitleForInterval(interval)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,14 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
<Controller
|
<Controller
|
||||||
name="enabled"
|
name="enabled"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => <Checkbox {...field} title={t('query_log_enable')} disabled={processing} />}
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid="logs_enabled"
|
||||||
|
title={t('query_log_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,6 +104,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="logs_anonymize_client_ip"
|
||||||
title={t('anonymize_client_ip')}
|
title={t('anonymize_client_ip')}
|
||||||
subtitle={t('anonymize_client_ip_desc')}
|
subtitle={t('anonymize_client_ip_desc')}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@@ -114,6 +122,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
<label className="custom-control custom-radio">
|
<label className="custom-control custom-radio">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
data-testid="logs_config_interval"
|
||||||
className="custom-control-input"
|
className="custom-control-input"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
checked={!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}
|
checked={!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}
|
||||||
@@ -128,7 +137,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
|
|
||||||
{!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue) && (
|
{!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue) && (
|
||||||
<div className="form__group--input">
|
<div className="form__group--input">
|
||||||
<div className="form__desc form__desc--top">{i18next.t('custom_rotation_input')}</div>
|
<div className="form__desc form__desc--top">{t('custom_rotation_input')}</div>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="customInterval"
|
name="customInterval"
|
||||||
@@ -136,7 +145,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
placeholder={t('encryption_certificates_input')}
|
data-testid="logs_config_custom_interval"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
min={RETENTION_RANGE.MIN}
|
min={RETENTION_RANGE.MIN}
|
||||||
@@ -156,6 +165,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
className="custom-control-input"
|
className="custom-control-input"
|
||||||
|
data-testid={`logs_config_${interval}`}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
value={interval}
|
value={interval}
|
||||||
checked={intervalValue === interval}
|
checked={intervalValue === interval}
|
||||||
@@ -185,6 +195,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="logs_config_ingored"
|
||||||
placeholder={t('ignore_domains')}
|
placeholder={t('ignore_domains')}
|
||||||
className="text-input"
|
className="text-input"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@@ -196,12 +207,17 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<button type="submit" className="btn btn-success btn-standard btn-large" disabled={disableSubmit}>
|
<button
|
||||||
|
type="submit"
|
||||||
|
data-testid="logs_config_save"
|
||||||
|
className="btn btn-success btn-standard btn-large"
|
||||||
|
disabled={disableSubmit}>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="logs_config_clear"
|
||||||
className="btn btn-outline-secondary btn-standard form__button"
|
className="btn btn-outline-secondary btn-standard form__button"
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
disabled={processingReset}>
|
disabled={processingReset}>
|
||||||
|
|||||||
@@ -82,7 +82,14 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
<Controller
|
<Controller
|
||||||
name="enabled"
|
name="enabled"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => <Checkbox {...field} title={t('statistics_enable')} disabled={processing} />}
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid="stats_config_enabled"
|
||||||
|
title={t('statistics_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,6 +106,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
<label className="custom-control custom-radio">
|
<label className="custom-control custom-radio">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
data-testid="stats_config_interval"
|
||||||
className="custom-control-input"
|
className="custom-control-input"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}
|
checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}
|
||||||
@@ -121,7 +129,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
placeholder={t('encryption_certificates_input')}
|
data-testid="stats_config_custom_interval"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
min={RETENTION_RANGE.MIN}
|
min={RETENTION_RANGE.MIN}
|
||||||
@@ -169,6 +177,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="stats_config_ignored"
|
||||||
placeholder={t('ignore_domains')}
|
placeholder={t('ignore_domains')}
|
||||||
className="text-input"
|
className="text-input"
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@@ -180,12 +189,17 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<button type="submit" className="btn btn-success btn-standard btn-large" disabled={disableSubmit}>
|
<button
|
||||||
|
type="submit"
|
||||||
|
data-testid="stats_config_save"
|
||||||
|
className="btn btn-success btn-standard btn-large"
|
||||||
|
disabled={disableSubmit}>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="stats_config_clear"
|
||||||
className="btn btn-outline-secondary btn-standard form__button"
|
className="btn btn-outline-secondary btn-standard form__button"
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
disabled={processingReset}>
|
disabled={processingReset}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||||||
|
|
||||||
type Props = ComponentProps<'input'> & {
|
type Props = ComponentProps<'input'> & {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
desc?: string;
|
||||||
leftAddon?: ReactNode;
|
leftAddon?: ReactNode;
|
||||||
rightAddon?: ReactNode;
|
rightAddon?: ReactNode;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -10,13 +11,14 @@ type Props = ComponentProps<'input'> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, Props>(
|
export const Input = forwardRef<HTMLInputElement, Props>(
|
||||||
({ name, label, className, leftAddon, rightAddon, error, trimOnBlur, onBlur, ...rest }, ref) => (
|
({ name, label, desc, className, leftAddon, rightAddon, error, trimOnBlur, onBlur, ...rest }, ref) => (
|
||||||
<div className={clsx('form-group', { 'has-error': !!error })}>
|
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||||
{label && (
|
{label && (
|
||||||
<label className="form__label" htmlFor={name}>
|
<label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
{desc && <div className="form__desc form__desc--top">{desc}</div>}
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
{leftAddon && <div>{leftAddon}</div>}
|
{leftAddon && <div>{leftAddon}</div>}
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const Radio = forwardRef<HTMLInputElement, Props<string | boolean | numbe
|
|||||||
className="custom-control custom-radio">
|
className="custom-control custom-radio">
|
||||||
<input
|
<input
|
||||||
id={getId(o.label)}
|
id={getId(o.label)}
|
||||||
|
data-testid={o.value}
|
||||||
type="radio"
|
type="radio"
|
||||||
className="custom-control-input"
|
className="custom-control-input"
|
||||||
onChange={() => onChange(o.value)}
|
onChange={() => onChange(o.value)}
|
||||||
|
|||||||
@@ -4,19 +4,22 @@ import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
|||||||
|
|
||||||
type Props = ComponentProps<'textarea'> & {
|
type Props = ComponentProps<'textarea'> & {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
wrapperClassName?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
desc?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
trimOnBlur?: boolean;
|
trimOnBlur?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
|
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
|
||||||
({ name, label, className, error, trimOnBlur, onBlur, ...rest }, ref) => (
|
({ name, label, desc, className, wrapperClassName, error, trimOnBlur, onBlur, ...rest }, ref) => (
|
||||||
<div className={clsx('form-group', { 'has-error': !!error })}>
|
<div className={clsx('form-group', wrapperClassName, { 'has-error': !!error })}>
|
||||||
{label && (
|
{label && (
|
||||||
<label className="form__label" htmlFor={name}>
|
<label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
{desc && <div className="form__desc form__desc--top">{desc}</div>}
|
||||||
<textarea
|
<textarea
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'form-control form-control--textarea form-control--textarea-small font-monospace',
|
'form-control form-control--textarea form-control--textarea-small font-monospace',
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
|
|||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="clientId" className="form__label form__label--with-desc">
|
<label htmlFor="clientId" className="form__label form__label--with-desc">
|
||||||
{i18next.t('client_id')}
|
{t('client_id')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
@@ -176,8 +176,8 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Select {...field} label={t('protocol')} data-testid="mobile_config_protocol">
|
<Select {...field} label={t('protocol')} data-testid="mobile_config_protocol">
|
||||||
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
|
<option value={MOBILE_CONFIG_LINKS.DOT}>{t('dns_over_tls')}</option>
|
||||||
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
|
<option value={MOBILE_CONFIG_LINKS.DOH}>{t('dns_over_https')}</option>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ export type DnsConfigData = {
|
|||||||
ratelimit_subnet_len_ipv4?: number;
|
ratelimit_subnet_len_ipv4?: number;
|
||||||
ratelimit_subnet_len_ipv6?: number;
|
ratelimit_subnet_len_ipv6?: number;
|
||||||
edns_cs_use_custom?: boolean;
|
edns_cs_use_custom?: boolean;
|
||||||
edns_cs_custom_ip?: boolean;
|
edns_cs_custom_ip?: string;
|
||||||
cache_size?: number;
|
cache_size?: number;
|
||||||
cache_ttl_max?: number;
|
cache_ttl_max?: number;
|
||||||
cache_ttl_min?: number;
|
cache_ttl_min?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user