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