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 { 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 && <>&nbsp;({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')}

View File

@@ -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')}

View File

@@ -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')}

View File

@@ -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')}

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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}>

View File

@@ -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

View File

@@ -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)}

View File

@@ -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',

View File

@@ -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>
)} )}
/> />

View File

@@ -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;