add encryption form and common components

This commit is contained in:
Ildar Kamalov
2025-01-18 19:21:20 +03:00
parent b4aa411826
commit 93890c2c6f
9 changed files with 525 additions and 422 deletions

View File

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

View File

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

View File

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

View File

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

View 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';

View 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';

View 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';

View File

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

View File

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