fix forms

This commit is contained in:
Ildar Kamalov
2025-01-27 13:29:07 +03:00
parent 09b15210ed
commit 6cb3d85d01
12 changed files with 404 additions and 318 deletions

View File

@@ -1,47 +1,67 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { CLIENT_ID_LINK } from '../../../../helpers/constants';
import { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers';
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',
title: 'access_allowed_title',
subtitle: 'access_allowed_desc',
title: i18next.t('access_allowed_title'),
subtitle: (
<Trans
components={{
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
}}>
access_allowed_desc
</Trans>
),
normalizeOnBlur: removeEmptyLines,
},
{
id: 'disallowed_clients',
title: 'access_disallowed_title',
subtitle: 'access_disallowed_desc',
title: i18next.t('access_disallowed_title'),
subtitle: (
<Trans
components={{
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
}}>
access_disallowed_desc
</Trans>
),
normalizeOnBlur: trimMultilineString,
},
{
id: 'blocked_hosts',
title: 'access_blocked_title',
subtitle: 'access_blocked_desc',
title: i18next.t('access_blocked_title'),
subtitle: i18next.t('access_blocked_desc'),
normalizeOnBlur: removeEmptyLines,
},
];
interface FormProps {
type FormProps = {
initialValues?: {
allowed_clients?: string;
disallowed_clients?: string;
blocked_hosts?: string;
};
onSubmit: (data: any) => void;
onSubmit: (data: FormData) => void;
processingSet: boolean;
}
interface FormData {
allowed_clients: string;
disallowed_clients: string;
blocked_hosts: string;
}
};
const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
const { t } = useTranslation();
@@ -70,7 +90,7 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
}: {
id: keyof FormData;
title: string;
subtitle: string;
subtitle: ReactNode;
normalizeOnBlur: (value: string) => string;
}) => {
const disabled = allowedClients && id === 'disallowed_clients';
@@ -78,22 +98,11 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
return (
<div key={id} className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor={id}>
{t(title)}
{disabled && (
<>
<span> </span>({t('disabled')})
</>
)}
{title}
{disabled && <>&nbsp;({t('disabled')})</>}
</label>
<div className="form__desc form__desc--top">
<Trans
components={{
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
}}>
{subtitle}
</Trans>
</div>
<div className="form__desc form__desc--top">{subtitle}</div>
<Controller
name={id}
@@ -102,6 +111,7 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
<Textarea
{...field}
id={id}
data-testid={id}
disabled={disabled || processingSet}
onBlur={(e) => {
field.onChange(normalizeOnBlur(e.target.value));
@@ -115,21 +125,13 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((f) =>
renderField(
f as {
id: keyof FormData;
title: string;
subtitle: string;
normalizeOnBlur: (value: string) => string;
},
),
)}
{fields.map((f) => renderField(f))}
<div className="card-actions">
<div className="btn-list">
<button
type="submit"
data-testid="access_save"
className="btn btn-success btn-standard"
disabled={isSubmitting || !isDirty || processingSet}>
{t('save_config')}

View File

@@ -1,31 +1,33 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import i18next from 'i18next';
import { clearDnsCache } from '../../../../actions/dnsConfig';
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from '../../../../helpers/constants';
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
import { RootState } from '../../../../initialState';
import { Checkbox } from '../../../ui/Controls/Checkbox';
const INPUTS_FIELDS = [
{
name: CACHE_CONFIG_FIELDS.cache_size,
title: 'cache_size',
description: 'cache_size_desc',
placeholder: 'enter_cache_size',
title: i18next.t('cache_size'),
description: i18next.t('cache_size_desc'),
placeholder: i18next.t('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',
title: i18next.t('cache_ttl_min_override'),
description: i18next.t('cache_ttl_min_override_desc'),
placeholder: i18next.t('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',
title: i18next.t('cache_ttl_max_override'),
description: i18next.t('cache_ttl_max_override_desc'),
placeholder: i18next.t('enter_cache_ttl_max_override'),
},
];
@@ -51,6 +53,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
register,
handleSubmit,
watch,
control,
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onBlur',
@@ -81,15 +84,16 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
<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)}
{title}
</label>
<div className="form__desc form__desc--top">{t(description)}</div>
<div className="form__desc form__desc--top">{description}</div>
<input
type="number"
data-testid={`dns_${name}`}
className="form-control"
placeholder={t(placeholder)}
placeholder={placeholder}
disabled={processingSetConfig}
min={0}
max={UINT32_RANGE.MAX}
@@ -108,26 +112,26 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
<div className="row">
<div className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label className="checkbox">
<input
type="checkbox"
className="checkbox__input"
disabled={processingSetConfig}
{...register('cache_optimistic')}
/>
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{t('cache_optimistic')}</span>
<span className="checkbox__label-subtitle">{t('cache_optimistic_desc')}</span>
</span>
</span>
</label>
<Controller
name="cache_optimistic"
control={control}
render={({ field }) => (
<Checkbox
{...field}
data-testid="dns_cache_optimistic"
title={t('cache_optimistic')}
subtitle={t('cache_optimistic_desc')}
disabled={processingSetConfig}
/>
)}
/>
</div>
</div>
</div>
<button
type="submit"
data-testid="dns_save"
className="btn btn-success btn-standard btn-large"
disabled={isSubmitting || !isDirty || processingSetConfig || minExceedsMax}>
{t('save_btn')}
@@ -135,6 +139,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
<button
type="button"
data-testid="dns_clear"
className="btn btn-outline-secondary btn-standard form__button"
onClick={handleClearCache}>
{t('clear_cache')}

View File

@@ -6,8 +6,11 @@ import i18next from 'i18next';
import { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from '../../../../helpers/validators';
import { BLOCKING_MODES, UINT32_RANGE } from '../../../../helpers/constants';
import { removeEmptyLines } from '../../../../helpers/helpers';
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: {
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',
label: i18next.t('blocking_ipv4'),
description: i18next.t('blocking_ipv4_desc'),
validateIp: validateIpv4,
},
{
description: 'blocking_ipv6_desc',
name: 'blocking_ipv6',
label: i18next.t('blocking_ipv6'),
description: i18next.t('blocking_ipv6_desc'),
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 = {
ratelimit: number;
ratelimit_subnet_len_ipv4: number;
@@ -46,7 +87,7 @@ type FormData = {
ratelimit_whitelist: string;
edns_cs_enabled: boolean;
edns_cs_use_custom: boolean;
edns_cs_custom_ip?: boolean;
edns_cs_custom_ip?: string;
dnssec_enabled: boolean;
disable_ipv6: boolean;
blocking_mode: string;
@@ -65,11 +106,10 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
const { t } = useTranslation();
const {
register,
handleSubmit,
watch,
control,
formState: { errors, isSubmitting, isDirty },
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onBlur',
defaultValues: initialValues,
@@ -84,107 +124,102 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
<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">
{t('rate_limit')}
</label>
<div className="form__desc form__desc--top">{t('rate_limit_desc')}</div>
<input
id="ratelimit"
type="number"
className="form-control"
disabled={processing}
{...register('ratelimit', {
required: t('form_error_required'),
valueAsNumber: true,
min: UINT32_RANGE.MIN,
max: UINT32_RANGE.MAX,
})}
<Controller
name="ratelimit"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_ratelimit"
type="number"
label={t('rate_limit')}
desc={t('rate_limit_desc')}
error={fieldState.error?.message}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
disabled={processing}
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 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">
{t('rate_limit_subnet_len_ipv4')}
</label>
<div className="form__desc form__desc--top">{t('rate_limit_subnet_len_ipv4_desc')}</div>
<input
id="ratelimit_subnet_len_ipv4"
type="number"
className="form-control"
disabled={processing}
{...register('ratelimit_subnet_len_ipv4', {
required: t('form_error_required'),
valueAsNumber: true,
min: 0,
max: 32,
})}
<Controller
name="ratelimit_subnet_len_ipv4"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_subnet_ipv4"
type="number"
label={t('rate_limit_subnet_len_ipv4')}
desc={t('rate_limit_subnet_len_ipv4_desc')}
error={fieldState.error?.message}
min={0}
max={32}
disabled={processing}
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 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">
{t('rate_limit_subnet_len_ipv6')}
</label>
<div className="form__desc form__desc--top">{t('rate_limit_subnet_len_ipv6_desc')}</div>
<input
id="ratelimit_subnet_len_ipv6"
type="number"
className="form-control"
disabled={processing}
{...register('ratelimit_subnet_len_ipv6', {
required: t('form_error_required'),
valueAsNumber: true,
min: 0,
max: 128,
})}
<Controller
name="ratelimit_subnet_len_ipv6"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_subnet_ipv6"
type="number"
label={t('rate_limit_subnet_len_ipv6')}
desc={t('rate_limit_subnet_len_ipv6_desc')}
error={fieldState.error?.message}
min={0}
max={128}
disabled={processing}
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 className="col-12 col-md-7">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit_whitelist" className="form__label form__label--with-desc">
{t('rate_limit_whitelist')}
</label>
<div className="form__desc form__desc--top">{t('rate_limit_whitelist_desc')}</div>
<textarea
id="ratelimit_whitelist"
className="form-control"
disabled={processing}
{...register('ratelimit_whitelist', {
onChange: removeEmptyLines,
})}
<Controller
name="ratelimit_whitelist"
control={control}
render={({ field, fieldState }) => (
<Textarea
{...field}
data-testid="dns_config_subnet_ipv6"
label={t('rate_limit_whitelist')}
desc={t('rate_limit_whitelist_desc')}
error={fieldState.error?.message}
disabled={processing}
trimOnBlur
/>
)}
/>
{errors.ratelimit_whitelist && (
<div className="form__message form__message--error">
{errors.ratelimit_whitelist.message}
</div>
)}
</div>
</div>
@@ -194,7 +229,12 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
name="edns_cs_enabled"
control={control}
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>
@@ -208,6 +248,7 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
render={({ field }) => (
<Checkbox
{...field}
data-testid="dns_config_edns_use_custom_ip"
title={t('edns_use_custom_ip')}
disabled={processing || !edns_cs_enabled}
/>
@@ -216,15 +257,23 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
</div>
{edns_cs_use_custom && (
<input
id="edns_cs_custom_ip"
type="text"
className="form-control"
disabled={processing || !edns_cs_enabled}
{...register('edns_cs_custom_ip', {
required: t('form_error_required'),
validate: (value) => validateIp(value) || validateRequiredValue(value),
})}
<Controller
name="edns_cs_custom_ip"
control={control}
rules={{
validate: {
required: validateRequiredValue,
id: validateIp,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_edns_cs_custom_ip"
error={fieldState.error?.message}
disabled={processing || !edns_cs_enabled}
/>
)}
/>
)}
</div>
@@ -238,6 +287,7 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
render={({ field }) => (
<Checkbox
{...field}
data-testid={`dns_config_${name}`}
title={placeholder}
subtitle={subtitle}
disabled={processing}
@@ -253,53 +303,52 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
<label className="form__label form__label--with-desc">{t('blocking_mode')}</label>
<div className="form__desc form__desc--top">
{Object.values(BLOCKING_MODES).map((mode: any) => (
<li key={mode}>{t(`blocking_mode_${mode}`)}</li>
{blockingModeDescriptions.map((desc: string) => (
<li key={desc}>{desc}</li>
))}
</div>
<div className="custom-controls-stacked">
{Object.values(BLOCKING_MODES).map((mode: any) => (
<label key={mode} className="custom-control custom-radio">
<input
type="radio"
className="custom-control-input"
value={mode}
disabled={processing}
{...register('blocking_mode')}
/>
<span className="custom-control-label">{t(mode)}</span>
</label>
))}
<Controller
name="blocking_mode"
control={control}
render={({ field }) => (
<Radio {...field} options={blockingModeOptions} disabled={processing} />
)}
/>
</div>
</div>
</div>
{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="form__group form__group--settings">
<label className="form__label form__label--with-desc" htmlFor={name}>
{t(name)}
</label>
<div className="form__desc form__desc--top">{t(description)}</div>
<input
id={name}
type="text"
className="form-control"
disabled={processing}
{...register(name as keyof FormData, {
required: t('form_error_required'),
validate: (value) => validateIp(value) || validateRequiredValue(value),
})}
<Controller
name={name}
control={control}
rules={{
validate: {
required: validateRequiredValue,
ip: validateIp,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_blocked_response_ttl"
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>
))}
@@ -308,35 +357,35 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
<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">
{t('blocked_response_ttl')}
</label>
<div className="form__desc form__desc--top">{t('blocked_response_ttl_desc')}</div>
<input
id="blocked_response_ttl"
type="number"
className="form-control"
disabled={processing}
{...register('blocked_response_ttl', {
required: t('form_error_required'),
valueAsNumber: true,
min: UINT32_RANGE.MIN,
max: UINT32_RANGE.MAX,
})}
<Controller
name="blocked_response_ttl"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_blocked_response_ttl"
type="number"
label={t('blocked_response_ttl')}
desc={t('blocked_response_ttl_desc')}
error={fieldState.error?.message}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
disabled={processing}
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>
<button
type="submit"
data-testid="dns_config_save"
className="btn btn-success btn-standard btn-large"
disabled={isSubmitting || !isDirty || processing}>
{t('save_btn')}

View File

@@ -1,9 +1,10 @@
import classnames from 'classnames';
import React, { useRef } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import i18next from 'i18next';
import clsx from 'clsx';
import { testUpstreamWithFormValues } from '../../../../actions';
import { DNS_REQUEST_OPTIONS, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';
import { removeEmptyLines } from '../../../../helpers/helpers';
@@ -12,9 +13,10 @@ import { RootState } from '../../../../initialState';
import '../../../ui/texareaCommentsHighlight.css';
import Examples from './Examples';
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_MODE_NAME = 'upstream_mode';
type FormData = {
upstream_dns: string;
@@ -31,24 +33,21 @@ type FormProps = {
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,
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,
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,
subtitle: 'fastest_addr_desc',
placeholder: 'fastest_addr',
},
];
@@ -91,14 +90,10 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
dispatch(testUpstreamWithFormValues(formValues));
};
const testButtonClass = classnames('btn btn-primary btn-standard mr-2', {
'btn-loading': processingTestUpstream,
});
return (
<form onSubmit={handleSubmit(onSubmit)} className="form--upstream">
<div className="row">
<label className="col form__label" htmlFor={UPSTREAM_DNS_NAME}>
<label className="col form__label" htmlFor="upstream_dns">
<Trans
components={{
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
@@ -126,17 +121,16 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
control={control}
render={({ field }) => (
<>
<textarea
<Textarea
{...field}
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')}
disabled={!!upstream_dns_file || processingSetConfig || processingTestUpstream}
onScroll={(e) => syncScroll(e, textareaRef)}
onBlur={(e) => {
const value = removeEmptyLines(e.target.value);
field.onChange(value);
}}
trimOnBlur
/>
{getTextareaCommentsHighlight(textareaRef, upstream_dns)}
</>
@@ -150,31 +144,19 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
<hr />
</div>
{INPUT_FIELDS.map(({ name, value, subtitle, placeholder }) => (
<div key={value} className="col-12 mb-4">
<Controller
name="upstream_mode"
control={control}
render={({ field }) => (
<div className="custom-control custom-radio">
<input
{...field}
type="radio"
className="custom-control-input"
id={`${name}_${value}`}
value={value}
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 mb-4">
<Controller
name="upstream_mode"
control={control}
render={({ field }) => (
<Radio
{...field}
options={upstreamModeOptions}
disabled={processingSetConfig || processingTestUpstream}
/>
)}
/>
</div>
<div className="col-12">
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
@@ -187,16 +169,14 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
name="fallback_dns"
control={control}
render={({ field }) => (
<textarea
<Textarea
{...field}
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')}
disabled={processingSetConfig}
onBlur={(e) => {
const value = removeEmptyLines(e.target.value);
field.onChange(value);
}}
trimOnBlur
/>
)}
/>
@@ -206,7 +186,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
<hr />
</div>
<div className="col-12 mb-2">
<div className="col-12">
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
{t('bootstrap_dns')}
</label>
@@ -217,11 +197,12 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
name="bootstrap_dns"
control={control}
render={({ field }) => (
<textarea
<Textarea
{...field}
id="bootstrap_dns"
className="form-control form-control--textarea form-control--textarea-small font-monospace"
data-testid="bootstrap_dns"
placeholder={t('bootstrap_dns')}
wrapperClassName="mb-0"
disabled={processingSetConfig}
onBlur={(e) => {
const value = removeEmptyLines(e.target.value);
@@ -255,16 +236,13 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
name="local_ptr_upstreams"
control={control}
render={({ field }) => (
<textarea
<Textarea
{...field}
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')}
disabled={processingSetConfig}
onBlur={(e) => {
const value = removeEmptyLines(e.target.value);
field.onChange(value);
}}
trimOnBlur
/>
)}
/>
@@ -276,6 +254,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
render={({ field }) => (
<Checkbox
{...field}
data-testid="dns_use_private_ptr_resolvers"
title={t('use_private_ptr_resolvers_title')}
subtitle={t('use_private_ptr_resolvers_desc')}
disabled={processingSetConfig}
@@ -296,6 +275,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
render={({ field }) => (
<Checkbox
{...field}
data-testid="dns_resolve_clients"
title={t('resolve_clients_title')}
subtitle={t('resolve_clients_desc')}
disabled={processingSetConfig}
@@ -309,7 +289,10 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
<div className="btn-list">
<button
type="button"
className={testButtonClass}
data-testid="dns_upstream_test"
className={clsx('btn btn-primary btn-standard mr-2', {
'btn-loading': processingTestUpstream,
})}
onClick={handleUpstreamTest}
disabled={!upstream_dns || processingTestUpstream}>
{t('test_upstream_btn')}
@@ -317,6 +300,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
<button
type="submit"
data-testid="dns_upstream_save"
className="btn btn-success btn-standard"
disabled={isSubmitting || !isDirty || processingSetConfig || processingTestUpstream}>
{t('apply_btn')}

View File

@@ -6,6 +6,7 @@ import i18next from 'i18next';
import { toNumber } from '../../../helpers/form';
import { DAY, FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK } from '../../../helpers/constants';
import { Checkbox } from '../../ui/Controls/Checkbox';
import { Select } from '../../ui/Controls/Select';
const THREE_DAYS_INTERVAL = DAY * 3;
const SEVEN_DAYS_INTERVAL = DAY * 7;
@@ -37,7 +38,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
const { t } = useTranslation();
const prevFormValuesRef = useRef<FormValues>(initialValues);
const { register, watch, control } = useForm({
const { watch, control } = useForm({
mode: 'onBlur',
defaultValues: initialValues,
});
@@ -68,6 +69,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
render={({ field }) => (
<Checkbox
{...field}
data-testid="filters_enabled"
title={t('block_domain_use_filters_and_hosts')}
disabled={processing}
/>
@@ -85,18 +87,26 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
<label className="form__label">
<Trans>filters_interval</Trans>
</label>
<select
{...register('interval', {
setValueAs: toNumber,
})}
className="custom-select"
disabled={processing}>
{FILTERS_INTERVALS_HOURS.map((interval) => (
<option value={interval} key={interval}>
{getTitleForInterval(interval)}
</option>
))}
</select>
<Controller
name="interval"
control={control}
render={({ field }) => (
<Select
{...field}
data-testid="filters_interval"
disabled={processing}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}>
{FILTERS_INTERVALS_HOURS.map((interval) => (
<option value={interval} key={interval}>
{getTitleForInterval(interval)}
</option>
))}
</Select>
)}
/>
</div>
</div>
</div>

View File

@@ -86,7 +86,14 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<Controller
name="enabled"
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>
@@ -97,6 +104,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
render={({ field }) => (
<Checkbox
{...field}
data-testid="logs_anonymize_client_ip"
title={t('anonymize_client_ip')}
subtitle={t('anonymize_client_ip_desc')}
disabled={processing}
@@ -114,6 +122,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<label className="custom-control custom-radio">
<input
type="radio"
data-testid="logs_config_interval"
className="custom-control-input"
disabled={processing}
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) && (
<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
name="customInterval"
@@ -136,7 +145,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
render={({ field, fieldState }) => (
<Input
{...field}
placeholder={t('encryption_certificates_input')}
data-testid="logs_config_custom_interval"
disabled={processing}
error={fieldState.error?.message}
min={RETENTION_RANGE.MIN}
@@ -156,6 +165,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<input
type="radio"
className="custom-control-input"
data-testid={`logs_config_${interval}`}
disabled={processing}
value={interval}
checked={intervalValue === interval}
@@ -185,6 +195,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
render={({ field, fieldState }) => (
<Textarea
{...field}
data-testid="logs_config_ingored"
placeholder={t('ignore_domains')}
className="text-input"
disabled={processing}
@@ -196,12 +207,17 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
</div>
<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>
</button>
<button
type="button"
data-testid="logs_config_clear"
className="btn btn-outline-secondary btn-standard form__button"
onClick={onReset}
disabled={processingReset}>

View File

@@ -82,7 +82,14 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<Controller
name="enabled"
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>
@@ -99,6 +106,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<label className="custom-control custom-radio">
<input
type="radio"
data-testid="stats_config_interval"
className="custom-control-input"
disabled={processing}
checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}
@@ -121,7 +129,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
render={({ field, fieldState }) => (
<Input
{...field}
placeholder={t('encryption_certificates_input')}
data-testid="stats_config_custom_interval"
disabled={processing}
error={fieldState.error?.message}
min={RETENTION_RANGE.MIN}
@@ -169,6 +177,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
render={({ field, fieldState }) => (
<Textarea
{...field}
data-testid="stats_config_ignored"
placeholder={t('ignore_domains')}
className="text-input"
disabled={processing}
@@ -180,12 +189,17 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
</div>
<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>
</button>
<button
type="button"
data-testid="stats_config_clear"
className="btn btn-outline-secondary btn-standard form__button"
onClick={onReset}
disabled={processingReset}>

View File

@@ -3,6 +3,7 @@ import clsx from 'clsx';
type Props = ComponentProps<'input'> & {
label?: string;
desc?: string;
leftAddon?: ReactNode;
rightAddon?: ReactNode;
error?: string;
@@ -10,13 +11,14 @@ type Props = ComponentProps<'input'> & {
};
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 })}>
{label && (
<label className="form__label" htmlFor={name}>
<label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>
{label}
</label>
)}
{desc && <div className="form__desc form__desc--top">{desc}</div>}
<div className="input-group">
{leftAddon && <div>{leftAddon}</div>}
<input

View File

@@ -25,6 +25,7 @@ export const Radio = forwardRef<HTMLInputElement, Props<string | boolean | numbe
className="custom-control custom-radio">
<input
id={getId(o.label)}
data-testid={o.value}
type="radio"
className="custom-control-input"
onChange={() => onChange(o.value)}

View File

@@ -4,19 +4,22 @@ import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
type Props = ComponentProps<'textarea'> & {
className?: string;
wrapperClassName?: string;
label?: string;
desc?: string;
error?: string;
trimOnBlur?: boolean;
};
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
({ name, label, className, error, trimOnBlur, onBlur, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}>
({ name, label, desc, className, wrapperClassName, error, trimOnBlur, onBlur, ...rest }, ref) => (
<div className={clsx('form-group', wrapperClassName, { 'has-error': !!error })}>
{label && (
<label className="form__label" htmlFor={name}>
<label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>
{label}
</label>
)}
{desc && <div className="form__desc form__desc--top">{desc}</div>}
<textarea
className={clsx(
'form-control form-control--textarea form-control--textarea-small font-monospace',

View File

@@ -142,7 +142,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<div className="form__group form__group--settings">
<label htmlFor="clientId" className="form__label form__label--with-desc">
{i18next.t('client_id')}
{t('client_id')}
</label>
<div className="form__desc form__desc--top">
@@ -176,8 +176,8 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
control={control}
render={({ field }) => (
<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.DOH}>{i18next.t('dns_over_https')}</option>
<option value={MOBILE_CONFIG_LINKS.DOT}>{t('dns_over_tls')}</option>
<option value={MOBILE_CONFIG_LINKS.DOH}>{t('dns_over_https')}</option>
</Select>
)}
/>

View File

@@ -320,7 +320,7 @@ export type DnsConfigData = {
ratelimit_subnet_len_ipv4?: number;
ratelimit_subnet_len_ipv6?: number;
edns_cs_use_custom?: boolean;
edns_cs_custom_ip?: boolean;
edns_cs_custom_ip?: string;
cache_size?: number;
cache_ttl_max?: number;
cache_ttl_min?: number;