add encryption form and common components
This commit is contained in:
@@ -65,10 +65,12 @@ const Dhcp = () => {
|
||||
modalType,
|
||||
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
|
||||
|
||||
const interface_name =
|
||||
useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
|
||||
const isInterfaceIncludesIpv4 =
|
||||
useSelector((state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses);
|
||||
const interface_name = useSelector(
|
||||
(state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name,
|
||||
);
|
||||
const isInterfaceIncludesIpv4 = useSelector(
|
||||
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
|
||||
);
|
||||
|
||||
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
|
||||
|
||||
@@ -130,12 +132,7 @@ const Dhcp = () => {
|
||||
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
|
||||
|
||||
const getToggleDhcpButton = () => {
|
||||
const filledConfig =
|
||||
interface_name &&
|
||||
(Object.values(v4)
|
||||
|
||||
.every(Boolean) ||
|
||||
Object.values(v6).every(Boolean));
|
||||
const filledConfig = interface_name && (Object.values(v4).every(Boolean) || Object.values(v6).every(Boolean));
|
||||
|
||||
const className = classNames('btn btn-sm', {
|
||||
'btn-gray': enabled,
|
||||
@@ -200,11 +197,7 @@ const Dhcp = () => {
|
||||
|
||||
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
|
||||
const disabledLeasesButton = Boolean(
|
||||
dhcp?.syncErrors ||
|
||||
!isInterfaceIncludesIpv4 ||
|
||||
isEmptyConfig ||
|
||||
processingConfig ||
|
||||
!inputtedIPv4values,
|
||||
dhcp?.syncErrors || !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,
|
||||
);
|
||||
const cidr = inputtedIPv4values
|
||||
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { renderInputField, CheckboxField, renderRadioField, toNumber } from '../../../helpers/form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import i18next from 'i18next';
|
||||
import {
|
||||
validateServerName,
|
||||
validateIsSafePort,
|
||||
@@ -14,7 +12,6 @@ import {
|
||||
validatePortTLS,
|
||||
validatePlainDns,
|
||||
} from '../../../helpers/validators';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
import KeyStatus from './KeyStatus';
|
||||
|
||||
@@ -22,51 +19,37 @@ import CertificateStatus from './CertificateStatus';
|
||||
import {
|
||||
DNS_OVER_QUIC_PORT,
|
||||
DNS_OVER_TLS_PORT,
|
||||
FORM_NAME,
|
||||
STANDARD_HTTPS_PORT,
|
||||
ENCRYPTION_SOURCE,
|
||||
} from '../../../helpers/constants';
|
||||
import { Checkbox } from '../../ui/Controls/Checkbox';
|
||||
import { Radio } from '../../ui/Controls/Radio';
|
||||
import { Input } from '../../ui/Controls/Input';
|
||||
import { Textarea } from '../../ui/Controls/Textarea';
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
||||
const certificateSourceOptions = [
|
||||
{
|
||||
label: i18next.t('encryption_certificates_source_path'),
|
||||
value: ENCRYPTION_SOURCE.PATH,
|
||||
},
|
||||
{
|
||||
label: i18next.t('encryption_certificates_source_content'),
|
||||
value: ENCRYPTION_SOURCE.CONTENT,
|
||||
},
|
||||
];
|
||||
|
||||
if (values.port_dns_over_tls && values.port_https) {
|
||||
if (values.port_dns_over_tls === values.port_https) {
|
||||
errors.port_dns_over_tls = i18n.t('form_error_equal');
|
||||
const keySourceOptions = [
|
||||
{
|
||||
label: i18next.t('encryption_key_source_path'),
|
||||
value: ENCRYPTION_SOURCE.PATH,
|
||||
},
|
||||
{
|
||||
label: i18next.t('encryption_key_source_content'),
|
||||
value: ENCRYPTION_SOURCE.CONTENT,
|
||||
},
|
||||
];
|
||||
|
||||
errors.port_https = i18n.t('form_error_equal');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const clearFields = (change: any, setTlsConfig: any, validateTlsConfig: any, t: any) => {
|
||||
const fields = {
|
||||
private_key: '',
|
||||
certificate_chain: '',
|
||||
private_key_path: '',
|
||||
certificate_path: '',
|
||||
port_https: STANDARD_HTTPS_PORT,
|
||||
port_dns_over_tls: DNS_OVER_TLS_PORT,
|
||||
port_dns_over_quic: DNS_OVER_QUIC_PORT,
|
||||
server_name: '',
|
||||
force_https: false,
|
||||
enabled: false,
|
||||
private_key_saved: false,
|
||||
serve_plain_dns: true,
|
||||
};
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(t('encryption_reset'))) {
|
||||
Object.keys(fields)
|
||||
|
||||
.forEach((field) => change(field, fields[field]));
|
||||
setTlsConfig(fields);
|
||||
validateTlsConfig(fields);
|
||||
}
|
||||
};
|
||||
|
||||
const validationMessage = (warningValidation: any, isWarning: any) => {
|
||||
const validationMessage = (warningValidation: string, isWarning: boolean) => {
|
||||
if (!warningValidation) {
|
||||
return null;
|
||||
}
|
||||
@@ -88,19 +71,25 @@ const validationMessage = (warningValidation: any, isWarning: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface FormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
handleChange?: (...args: unknown[]) => unknown;
|
||||
isEnabled: boolean;
|
||||
servePlainDns: boolean;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
certificatePath: string;
|
||||
privateKeyPath: string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
initialValues: object;
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
serve_plain_dns: boolean;
|
||||
server_name: string;
|
||||
force_https: boolean;
|
||||
port_https: number;
|
||||
port_dns_over_tls: number;
|
||||
port_dns_over_quic: number;
|
||||
certificate_chain: string;
|
||||
private_key: string;
|
||||
certificate_path: string;
|
||||
private_key_path: string;
|
||||
certificate_source: string;
|
||||
key_source: string;
|
||||
private_key_saved: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialValues: FormValues;
|
||||
processingConfig: boolean;
|
||||
processingValidate: boolean;
|
||||
status_key?: string;
|
||||
@@ -114,71 +103,144 @@ interface FormProps {
|
||||
key_type?: string;
|
||||
issuer?: string;
|
||||
subject?: string;
|
||||
t: (...args: unknown[]) => string;
|
||||
setTlsConfig: (...args: unknown[]) => unknown;
|
||||
validateTlsConfig: (...args: unknown[]) => unknown;
|
||||
certificateSource?: string;
|
||||
privateKeySource?: string;
|
||||
privateKeySaved?: boolean;
|
||||
}
|
||||
onSubmit: (...args: unknown[]) => void;
|
||||
onChange: (...args: unknown[]) => void;
|
||||
setTlsConfig: (...args: unknown[]) => void;
|
||||
validateTlsConfig: (...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
enabled: false,
|
||||
serve_plain_dns: true,
|
||||
server_name: '',
|
||||
force_https: false,
|
||||
port_https: STANDARD_HTTPS_PORT,
|
||||
port_dns_over_tls: DNS_OVER_TLS_PORT,
|
||||
port_dns_over_quic: DNS_OVER_QUIC_PORT,
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
certificate_path: '',
|
||||
private_key_path: '',
|
||||
certificate_source: ENCRYPTION_SOURCE.PATH,
|
||||
key_source: ENCRYPTION_SOURCE.PATH,
|
||||
private_key_saved: false,
|
||||
};
|
||||
|
||||
export const Form = ({
|
||||
initialValues,
|
||||
processingConfig,
|
||||
processingValidate,
|
||||
not_after,
|
||||
valid_chain,
|
||||
valid_key,
|
||||
valid_cert,
|
||||
valid_pair,
|
||||
dns_names,
|
||||
key_type,
|
||||
issuer,
|
||||
subject,
|
||||
warning_validation,
|
||||
onSubmit,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const previousValuesRef = useRef<FormValues>(initialValues);
|
||||
|
||||
let Form = (props: FormProps) => {
|
||||
const {
|
||||
t,
|
||||
control,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
isEnabled,
|
||||
servePlainDns,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
certificatePath,
|
||||
privateKeyPath,
|
||||
change,
|
||||
invalid,
|
||||
submitting,
|
||||
processingConfig,
|
||||
processingValidate,
|
||||
not_after,
|
||||
valid_chain,
|
||||
valid_key,
|
||||
valid_cert,
|
||||
valid_pair,
|
||||
dns_names,
|
||||
key_type,
|
||||
issuer,
|
||||
subject,
|
||||
warning_validation,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
certificateSource,
|
||||
privateKeySource,
|
||||
privateKeySaved,
|
||||
} = props;
|
||||
watch,
|
||||
reset,
|
||||
setValue,
|
||||
setError,
|
||||
getValues,
|
||||
formState: { isSubmitting, isValid },
|
||||
} = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
...initialValues,
|
||||
...defaultValues,
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const watchedValues = watch();
|
||||
|
||||
const isEnabled = watch('enabled');
|
||||
const servePlainDns = watch('serve_plain_dns');
|
||||
const certificateChain = watch('certificate_chain');
|
||||
const privateKey = watch('private_key');
|
||||
const certificatePath = watch('certificate_path');
|
||||
const privateKeyPath = watch('private_key_path');
|
||||
const certificateSource = watch('certificate_source');
|
||||
const privateKeySaved = watch('private_key_saved');
|
||||
const privateKeySource = watch('key_source');
|
||||
|
||||
useEffect(() => {
|
||||
const previousValues = previousValuesRef.current;
|
||||
|
||||
if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) {
|
||||
// TODO onChange TLS config validation
|
||||
console.log('TLS config validation');
|
||||
previousValuesRef.current = watchedValues;
|
||||
}
|
||||
}, [watchedValues]);
|
||||
|
||||
const isSavingDisabled = () => {
|
||||
const processing = submitting || processingConfig || processingValidate;
|
||||
const processing = isSubmitting || processingConfig || processingValidate;
|
||||
|
||||
if (servePlainDns && !isEnabled) {
|
||||
return invalid || processing;
|
||||
return !isValid || processing;
|
||||
}
|
||||
|
||||
return invalid || processing || !valid_key || !valid_cert || !valid_pair;
|
||||
return !isValid || processing || !valid_key || !valid_cert || !valid_pair;
|
||||
};
|
||||
|
||||
const clearFields = () => {
|
||||
if (window.confirm(t('encryption_reset'))) {
|
||||
reset();
|
||||
setTlsConfig(defaultValues);
|
||||
validateTlsConfig(defaultValues);
|
||||
}
|
||||
};
|
||||
|
||||
const validatePorts = (values: FormValues) => {
|
||||
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
||||
|
||||
if (values.port_dns_over_tls && values.port_https) {
|
||||
if (values.port_dns_over_tls === values.port_https) {
|
||||
errors.port_dns_over_tls = i18next.t('form_error_equal');
|
||||
errors.port_https = i18next.t('form_error_equal');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const onFormSubmit = (data: FormValues) => {
|
||||
const validationErrors = validatePorts(data);
|
||||
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
Object.entries(validationErrors).forEach(([field, message]) => {
|
||||
setError(field as keyof FormValues, { type: 'manual', message });
|
||||
});
|
||||
} else {
|
||||
onSubmit(data);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = isSavingDisabled();
|
||||
const isWarning = valid_key && valid_cert && valid_pair;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings mb-3">
|
||||
<Field
|
||||
<Controller
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('encryption_enable')}
|
||||
onChange={handleChange}
|
||||
control={control}
|
||||
render={({ field }) => <Checkbox {...field} title={t('encryption_enable')} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -187,13 +249,13 @@ let Form = (props: FormProps) => {
|
||||
</div>
|
||||
|
||||
<div className="form__group mb-3 mt-5">
|
||||
<Field
|
||||
<Controller
|
||||
name="serve_plain_dns"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('encryption_plain_dns_enable')}
|
||||
onChange={handleChange}
|
||||
validate={validatePlainDns}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => validatePlainDns(value, getValues()),
|
||||
}}
|
||||
render={({ field }) => <Checkbox {...field} title={t('encryption_plain_dns_enable')} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -212,16 +274,19 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
id="server_name"
|
||||
<Controller
|
||||
name="server_name"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_server_enter')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
validate={validateServerName}
|
||||
control={control}
|
||||
rules={{ validate: validateServerName }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_server_enter')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -232,13 +297,12 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="force_https"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('encryption_redirect')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox {...field} title={t('encryption_redirect')} disabled={!isEnabled} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -255,17 +319,19 @@ let Form = (props: FormProps) => {
|
||||
<Trans>encryption_https</Trans>
|
||||
</label>
|
||||
|
||||
<Field
|
||||
id="port_https"
|
||||
<Controller
|
||||
name="port_https"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_https')}
|
||||
validate={[validatePort, validateIsSafePort]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
rules={{ validate: { validatePort, validateIsSafePort } }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder={t('encryption_https')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -280,17 +346,19 @@ let Form = (props: FormProps) => {
|
||||
<Trans>encryption_dot</Trans>
|
||||
</label>
|
||||
|
||||
<Field
|
||||
id="port_dns_over_tls"
|
||||
<Controller
|
||||
name="port_dns_over_tls"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_dot')}
|
||||
validate={[validatePortTLS]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
rules={{ validate: validatePortTLS }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder={t('encryption_dot')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -305,17 +373,19 @@ let Form = (props: FormProps) => {
|
||||
<Trans>encryption_doq</Trans>
|
||||
</label>
|
||||
|
||||
<Field
|
||||
id="port_dns_over_quic"
|
||||
<Controller
|
||||
name="port_dns_over_quic"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_doq')}
|
||||
validate={[validatePortQuic]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
rules={{ validate: validatePortQuic }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder={t('encryption_doq')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -352,50 +422,42 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="form__inline mb-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
<Controller
|
||||
name="certificate_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="path"
|
||||
placeholder={t('encryption_certificates_source_path')}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="certificate_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="content"
|
||||
placeholder={t('encryption_certificates_source_content')}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio {...field} options={certificateSourceOptions} disabled={!isEnabled} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT && (
|
||||
<Field
|
||||
id="certificate_chain"
|
||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<Controller
|
||||
name="certificate_chain"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
placeholder={t('encryption_certificates_input')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t('encryption_certificates_input')}
|
||||
disabled={!isEnabled}
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{certificateSource === ENCRYPTION_SOURCE.PATH && (
|
||||
<Field
|
||||
id="certificate_path"
|
||||
) : (
|
||||
<Controller
|
||||
name="certificate_path"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_certificate_path')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_certificate_path')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -424,70 +486,64 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="form__inline mb-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
<Controller
|
||||
name="key_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value={ENCRYPTION_SOURCE.PATH}
|
||||
placeholder={t('encryption_key_source_path')}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="key_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value={ENCRYPTION_SOURCE.CONTENT}
|
||||
placeholder={t('encryption_key_source_content')}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio {...field} options={keySourceOptions} disabled={!isEnabled} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{privateKeySource === ENCRYPTION_SOURCE.PATH && (
|
||||
<Field
|
||||
{privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<>
|
||||
<Controller
|
||||
name="private_key_saved"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
title={t('use_saved_key')}
|
||||
disabled={!isEnabled}
|
||||
onChange={(checked: boolean) => {
|
||||
if (checked) {
|
||||
setValue('private_key', '');
|
||||
}
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="private_key"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t('encryption_key_input')}
|
||||
disabled={!isEnabled || privateKeySaved}
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Controller
|
||||
name="private_key_path"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_private_key_path')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_private_key_path')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{privateKeySource === ENCRYPTION_SOURCE.CONTENT && [
|
||||
<Field
|
||||
key="private_key_saved"
|
||||
name="private_key_saved"
|
||||
type="checkbox"
|
||||
className="form__group form__group--settings mb-2"
|
||||
component={CheckboxField}
|
||||
disabled={!isEnabled}
|
||||
placeholder={t('use_saved_key')}
|
||||
onChange={(event: any) => {
|
||||
if (event.target.checked) {
|
||||
change('private_key', '');
|
||||
}
|
||||
if (handleChange) {
|
||||
handleChange(event);
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
|
||||
<Field
|
||||
id="private_key"
|
||||
key="private_key"
|
||||
name="private_key"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
placeholder={t('encryption_key_input')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled || privateKeySaved}
|
||||
/>,
|
||||
]}
|
||||
</div>
|
||||
|
||||
<div className="form__status">
|
||||
@@ -505,44 +561,11 @@ let Form = (props: FormProps) => {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-standart"
|
||||
disabled={submitting || processingConfig}
|
||||
onClick={() => clearFields(change, setTlsConfig, validateTlsConfig, t)}>
|
||||
disabled={isSubmitting || processingConfig}
|
||||
onClick={clearFields}>
|
||||
<Trans>reset_settings</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const selector = formValueSelector(FORM_NAME.ENCRYPTION);
|
||||
|
||||
Form = connect((state) => {
|
||||
const isEnabled = selector(state, 'enabled');
|
||||
const servePlainDns = selector(state, 'serve_plain_dns');
|
||||
const certificateChain = selector(state, 'certificate_chain');
|
||||
const privateKey = selector(state, 'private_key');
|
||||
const certificatePath = selector(state, 'certificate_path');
|
||||
const privateKeyPath = selector(state, 'private_key_path');
|
||||
const certificateSource = selector(state, 'certificate_source');
|
||||
const privateKeySource = selector(state, 'key_source');
|
||||
const privateKeySaved = selector(state, 'private_key_saved');
|
||||
return {
|
||||
isEnabled,
|
||||
servePlainDns,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
certificatePath,
|
||||
privateKeyPath,
|
||||
certificateSource,
|
||||
privateKeySource,
|
||||
privateKeySaved,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withTranslation(),
|
||||
reduxForm({
|
||||
form: FORM_NAME.ENCRYPTION,
|
||||
validate,
|
||||
}),
|
||||
])(Form);
|
||||
|
||||
@@ -1,49 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { debounce } from 'lodash';
|
||||
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
||||
|
||||
import Form from './Form';
|
||||
|
||||
import { Form, FormValues } from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
|
||||
import Loading from '../../ui/Loading';
|
||||
import { EncryptionData } from '../../../initialState';
|
||||
|
||||
interface EncryptionProps {
|
||||
setTlsConfig: (...args: unknown[]) => unknown;
|
||||
validateTlsConfig: (...args: unknown[]) => unknown;
|
||||
type Props = {
|
||||
encryption: EncryptionData;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
setTlsConfig: (values: EncryptionData) => void;
|
||||
validateTlsConfig: (values: EncryptionData) => void;
|
||||
};
|
||||
|
||||
class Encryption extends Component<EncryptionProps> {
|
||||
componentDidMount() {
|
||||
const { validateTlsConfig, encryption } = this.props;
|
||||
export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (encryption.enabled) {
|
||||
validateTlsConfig(encryption);
|
||||
}
|
||||
}
|
||||
}, [encryption, validateTlsConfig]);
|
||||
|
||||
handleFormSubmit = (values: any) => {
|
||||
const submitValues = this.getSubmitValues(values);
|
||||
|
||||
this.props.setTlsConfig(submitValues);
|
||||
};
|
||||
|
||||
handleFormChange = debounce((values) => {
|
||||
const submitValues = this.getSubmitValues(values);
|
||||
|
||||
if (submitValues.enabled) {
|
||||
this.props.validateTlsConfig(submitValues);
|
||||
}
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
getInitialValues = (data: any) => {
|
||||
const getInitialValues = useCallback((data: any): FormValues => {
|
||||
const { certificate_chain, private_key, private_key_saved } = data;
|
||||
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
||||
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
||||
@@ -53,9 +34,9 @@ class Encryption extends Component<EncryptionProps> {
|
||||
certificate_source,
|
||||
key_source,
|
||||
};
|
||||
};
|
||||
}, []);
|
||||
|
||||
getSubmitValues = (values: any) => {
|
||||
const getSubmitValues = useCallback((values: any) => {
|
||||
const { certificate_source, key_source, private_key_saved, ...config } = values;
|
||||
|
||||
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
|
||||
@@ -76,63 +57,50 @@ class Encryption extends Component<EncryptionProps> {
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const { encryption, t } = this.props;
|
||||
const {
|
||||
enabled,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
port_dns_over_quic,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
certificate_path,
|
||||
private_key_path,
|
||||
private_key_saved,
|
||||
serve_plain_dns,
|
||||
} = encryption;
|
||||
const handleFormSubmit = useCallback(
|
||||
(values: any) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
setTlsConfig(submitValues);
|
||||
},
|
||||
[getSubmitValues, setTlsConfig],
|
||||
);
|
||||
|
||||
const initialValues = this.getInitialValues({
|
||||
enabled,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
port_dns_over_quic,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
certificate_path,
|
||||
private_key_path,
|
||||
private_key_saved,
|
||||
serve_plain_dns,
|
||||
});
|
||||
const handleChange = useCallback(
|
||||
debounce((values) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
|
||||
return (
|
||||
<div className="encryption">
|
||||
<PageTitle title={t('encryption_settings')} />
|
||||
if (submitValues.enabled) {
|
||||
validateTlsConfig(submitValues);
|
||||
}
|
||||
}, DEBOUNCE_TIMEOUT),
|
||||
[getSubmitValues, validateTlsConfig],
|
||||
);
|
||||
|
||||
{encryption.processing && <Loading />}
|
||||
{!encryption.processing && (
|
||||
<Card
|
||||
title={t('encryption_title')}
|
||||
subtitle={t('encryption_desc')}
|
||||
bodyType="card-body box-body--settings">
|
||||
<Form
|
||||
initialValues={initialValues}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
onChange={this.handleFormChange}
|
||||
setTlsConfig={this.props.setTlsConfig}
|
||||
validateTlsConfig={this.props.validateTlsConfig}
|
||||
{...this.props.encryption}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const initialValues = getInitialValues(encryption);
|
||||
|
||||
export default withTranslation()(Encryption);
|
||||
return (
|
||||
<div className="encryption">
|
||||
<PageTitle title={t('encryption_settings')} />
|
||||
|
||||
{encryption.processing ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Card
|
||||
title={t('encryption_title')}
|
||||
subtitle={t('encryption_desc')}
|
||||
bodyType="card-body box-body--settings">
|
||||
<Form
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
onChange={handleChange}
|
||||
setTlsConfig={setTlsConfig}
|
||||
validateTlsConfig={validateTlsConfig}
|
||||
{...encryption}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { forwardRef, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import './checkbox.css';
|
||||
@@ -10,26 +10,35 @@ type Props = {
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
onChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const Checkbox = ({ title, subtitle, value, name, disabled, className = 'checkbox--form', onChange }: Props) => (
|
||||
<label className={clsx('checkbox', className)}>
|
||||
<span className="checkbox__marker" />
|
||||
<input
|
||||
name={name}
|
||||
type="checkbox"
|
||||
className="checkbox__input"
|
||||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<span className="checkbox__label">
|
||||
<span className="checkbox__label-text checkbox__label-text--long">
|
||||
<span className="checkbox__label-title">{title}</span>
|
||||
export const Checkbox = forwardRef<HTMLInputElement, Props>(
|
||||
({ title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange }, ref) => (
|
||||
<>
|
||||
<label className={clsx('checkbox', className)}>
|
||||
<span className="checkbox__marker" />
|
||||
<input
|
||||
name={name}
|
||||
type="checkbox"
|
||||
className="checkbox__input"
|
||||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
ref={ref}
|
||||
/>
|
||||
<span className="checkbox__label">
|
||||
<span className="checkbox__label-text checkbox__label-text--long">
|
||||
<span className="checkbox__label-title">{title}</span>
|
||||
|
||||
{subtitle && <span className="checkbox__label-subtitle">{subtitle}</span>}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{subtitle && <span className="checkbox__label-subtitle">{subtitle}</span>}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{error && <div className="form__message form__message--error">{error}</div>}
|
||||
</>
|
||||
),
|
||||
);
|
||||
|
||||
Checkbox.displayName = 'Checkbox';
|
||||
|
||||
29
client/src/components/ui/Controls/Input.tsx
Normal file
29
client/src/components/ui/Controls/Input.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { ComponentProps, forwardRef, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface Props extends ComponentProps<'input'> {
|
||||
label?: string;
|
||||
leftAddon?: ReactNode;
|
||||
rightAddon?: ReactNode;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, Props>(
|
||||
({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => (
|
||||
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||
{label && (
|
||||
<label className="form__label" htmlFor={name}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div>
|
||||
{leftAddon && <div>{leftAddon}</div>}
|
||||
<input className={clsx('form-control', className)} ref={ref} {...rest} />
|
||||
{rightAddon && <div>{rightAddon}</div>}
|
||||
</div>
|
||||
{error && <div className="form__message form__message--error">{error}</div>}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
49
client/src/components/ui/Controls/Radio.tsx
Normal file
49
client/src/components/ui/Controls/Radio.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
type Props<T> = {
|
||||
name: string;
|
||||
value: T;
|
||||
onChange: (e: T) => void;
|
||||
options: { label: string; desc?: string; value: T }[];
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const Radio = forwardRef<HTMLInputElement, Props<string | boolean | number | undefined>>(
|
||||
({ disabled, onChange, value, options, name, error, ...rest }, ref) => {
|
||||
const getId = (label: string) => (name ? `${label}_${name}` : label);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{options.map((o) => {
|
||||
const checked = value === o.value;
|
||||
|
||||
return (
|
||||
<label
|
||||
key={`${getId(o.label)}`}
|
||||
htmlFor={getId(o.label)}
|
||||
className="custom-control custom-radio">
|
||||
<input
|
||||
id={getId(o.label)}
|
||||
type="radio"
|
||||
className="custom-control-input"
|
||||
onChange={() => onChange(o.value)}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
<span className="custom-control-label">{o.label}</span>
|
||||
|
||||
{o.desc && <span className="checkbox__label-subtitle">{o.desc}</span>}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{!disabled && error && <span className="form__message form__message--error">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Radio.displayName = 'Radio';
|
||||
32
client/src/components/ui/Controls/Textarea.tsx
Normal file
32
client/src/components/ui/Controls/Textarea.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { ComponentProps, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface Props extends ComponentProps<'textarea'> {
|
||||
className?: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
|
||||
({ name, label, className, error, onClick, ...rest }, ref) => (
|
||||
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||
{label && (
|
||||
<label className="form__label" htmlFor={name}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
'form-control form-control--textarea form-control--textarea-small font-monospace',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
{error && <div className="form__message form__message--error">{error}</div>}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
Textarea.displayName = 'Textarea';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
|
||||
|
||||
import Encryption from '../components/Settings/Encryption';
|
||||
import { Encryption } from '../components/Settings/Encryption';
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const { encryption } = state;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const validateRequiredValue = (value: any) => {
|
||||
if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) {
|
||||
return undefined;
|
||||
}
|
||||
return 'form_error_required';
|
||||
return i18next.t('form_error_required');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
|
||||
const { range_end, range_start } = allValues.v4;
|
||||
|
||||
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
|
||||
return 'greater_range_start_error';
|
||||
return i18next.t('greater_range_start_error');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -62,7 +62,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
|
||||
*/
|
||||
export const validateIpv4 = (value: any) => {
|
||||
if (value && !R_IPV4.test(value)) {
|
||||
return 'form_error_ip4_format';
|
||||
return i18next.t('form_error_ip4_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -107,16 +107,16 @@ export const validateNotInRange = (value: any, allValues: any) => {
|
||||
*/
|
||||
export const validateGatewaySubnetMask = (_: any, allValues: any) => {
|
||||
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
|
||||
return 'gateway_or_subnet_invalid';
|
||||
return i18next.t('gateway_or_subnet_invalid');
|
||||
}
|
||||
|
||||
const { subnet_mask, gateway_ip } = allValues.v4;
|
||||
|
||||
if (validateIpv4(gateway_ip)) {
|
||||
return 'gateway_or_subnet_invalid';
|
||||
return i18next.t('gateway_or_subnet_invalid');
|
||||
}
|
||||
|
||||
return parseSubnetMask(subnet_mask) ? undefined : 'gateway_or_subnet_invalid';
|
||||
return parseSubnetMask(subnet_mask) ? undefined : i18next.t('gateway_or_subnet_invalid');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -138,7 +138,7 @@ export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
||||
const subnetPrefix = parseSubnetMask(subnet_mask);
|
||||
|
||||
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
|
||||
return 'subnet_error';
|
||||
return i18next.t('subnet_error');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -164,7 +164,7 @@ export const validateClientId = (value: any) => {
|
||||
R_CLIENT_ID.test(formattedValue)
|
||||
)
|
||||
) {
|
||||
return 'form_error_client_id_format';
|
||||
return i18next.t('form_error_client_id_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -205,7 +205,7 @@ export const validateServerName = (value: any) => {
|
||||
*/
|
||||
export const validateIpv6 = (value: any) => {
|
||||
if (value && !R_IPV6.test(value)) {
|
||||
return 'form_error_ip6_format';
|
||||
return i18next.t('form_error_ip6_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -216,7 +216,7 @@ export const validateIpv6 = (value: any) => {
|
||||
*/
|
||||
export const validateIp = (value: any) => {
|
||||
if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) {
|
||||
return 'form_error_ip_format';
|
||||
return i18next.t('form_error_ip_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -227,7 +227,7 @@ export const validateIp = (value: any) => {
|
||||
*/
|
||||
export const validateMac = (value: any) => {
|
||||
if (value && !R_MAC.test(value)) {
|
||||
return 'form_error_mac_format';
|
||||
return i18next.t('form_error_mac_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -249,7 +249,7 @@ export const validatePort = (value: any) => {
|
||||
*/
|
||||
export const validateInstallPort = (value: any) => {
|
||||
if (value < 1 || value > MAX_PORT) {
|
||||
return 'form_error_port';
|
||||
return i18next.t('form_error_port');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -263,7 +263,7 @@ export const validatePortTLS = (value: any) => {
|
||||
return undefined;
|
||||
}
|
||||
if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
||||
return 'form_error_port_range';
|
||||
return i18next.t('form_error_port_range');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -313,7 +313,7 @@ export const validateAnswer = (value: any) => {
|
||||
*/
|
||||
export const validatePath = (value: any) => {
|
||||
if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) {
|
||||
return 'form_error_url_or_path_format';
|
||||
return i18next.t('form_error_url_or_path_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -401,7 +401,7 @@ export const validatePlainDns = (value: any, allValues: any) => {
|
||||
const { enabled } = allValues;
|
||||
|
||||
if (!enabled && !value) {
|
||||
return 'encryption_plain_dns_error';
|
||||
return i18next.t('encryption_plain_dns_error');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user