fix form validation mode
This commit is contained in:
2
client/package.json
vendored
2
client/package.json
vendored
@@ -12,7 +12,7 @@
|
|||||||
"lint:fix": "eslint './src/**/*.(ts|tsx)' --fix",
|
"lint:fix": "eslint './src/**/*.(ts|tsx)' --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:e2e": "npx playwright test install.spec.ts --headed && npx playwright test test/e2e/dashboard",
|
"test:e2e": "npx playwright test tests/e2e --headed",
|
||||||
"test:e2e:interactive": "npx playwright test --ui",
|
"test:e2e:interactive": "npx playwright test --ui",
|
||||||
"test:e2e:debug": "npx playwright test --debug",
|
"test:e2e:debug": "npx playwright test --debug",
|
||||||
"test:e2e:codegen": "npx playwright codegen",
|
"test:e2e:codegen": "npx playwright codegen",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const Check = ({ onSubmit }: Props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty, isValid },
|
formState: { isDirty, isValid },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
|
|||||||
control,
|
control,
|
||||||
formState: { isDirty, isSubmitting },
|
formState: { isDirty, isSubmitting },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
domain: currentRewrite?.domain || '',
|
domain: currentRewrite?.domain || '',
|
||||||
answer: currentRewrite?.answer || '',
|
answer: currentRewrite?.answer || '',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
|
|||||||
setValue,
|
setValue,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { register, watch, setValue } = useForm<FormValues>({
|
const { register, watch, setValue } = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
search: initialValues.search || DEFAULT_LOGS_FILTER.search,
|
search: initialValues.search || DEFAULT_LOGS_FILTER.search,
|
||||||
response_status: initialValues.response_status || DEFAULT_LOGS_FILTER.response_status,
|
response_status: initialValues.response_status || DEFAULT_LOGS_FILTER.response_status,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const Form = ({
|
|||||||
...defaultFormValues,
|
...defaultFormValues,
|
||||||
...initialValues,
|
...initialValues,
|
||||||
},
|
},
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type FormDHCPv4Props = {
|
|||||||
lease_duration: string;
|
lease_duration: string;
|
||||||
};
|
};
|
||||||
interfaces: any;
|
interfaces: any;
|
||||||
onSubmit?: (data: DhcpFormValues) => Promise<void> | void;
|
onSubmit?: (data: DhcpFormValues) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => {
|
const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => {
|
||||||
@@ -32,7 +32,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting, isValid },
|
formState: { errors, isSubmitting },
|
||||||
watch,
|
watch,
|
||||||
} = useFormContext<DhcpFormValues>();
|
} = useFormContext<DhcpFormValues>();
|
||||||
|
|
||||||
@@ -41,25 +41,20 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
|
|
||||||
const formValues = watch('v4');
|
const formValues = watch('v4');
|
||||||
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
||||||
|
const hasV4Errors = errors.v4 && Object.keys(errors.v4).length > 0;
|
||||||
const handleFormSubmit = async (data: DhcpFormValues) => {
|
|
||||||
// TODO handle submit
|
|
||||||
if (onSubmit) {
|
|
||||||
await onSubmit(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDisabled = useMemo(() => {
|
const isDisabled = useMemo(() => {
|
||||||
return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig;
|
return isSubmitting || hasV4Errors || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig;
|
||||||
}, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]);
|
}, [isSubmitting, hasV4Errors, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_gateway_input')}</label>
|
<label>{t('dhcp_form_gateway_input')}</label>
|
||||||
<input
|
<input
|
||||||
|
data-testid="v4_gateway_ip"
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv4placeholders?.gateway_ip || '')}
|
placeholder={t(ipv4placeholders?.gateway_ip || '')}
|
||||||
@@ -80,6 +75,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_subnet_input')}</label>
|
<label>{t('dhcp_form_subnet_input')}</label>
|
||||||
<input
|
<input
|
||||||
|
data-testid="v4_subnet_mask"
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv4placeholders?.subnet_mask || '')}
|
placeholder={t(ipv4placeholders?.subnet_mask || '')}
|
||||||
@@ -106,6 +102,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<input
|
<input
|
||||||
|
data-testid="v4_range_start"
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv4placeholders?.range_start || '')}
|
placeholder={t(ipv4placeholders?.range_start || '')}
|
||||||
@@ -126,6 +123,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<input
|
<input
|
||||||
|
data-testid="v4_range_end"
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv4placeholders?.range_end || '')}
|
placeholder={t(ipv4placeholders?.range_end || '')}
|
||||||
@@ -150,6 +148,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_lease_title')}</label>
|
<label>{t('dhcp_form_lease_title')}</label>
|
||||||
<input
|
<input
|
||||||
|
data-testid="v4_lease_duration"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv4placeholders?.lease_duration || '')}
|
placeholder={t(ipv4placeholders?.lease_duration || '')}
|
||||||
@@ -173,7 +172,11 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button type="submit" className="btn btn-success btn-standard" disabled={isDisabled}>
|
<button
|
||||||
|
data-testid="v4_save"
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={isDisabled}>
|
||||||
{t('save_config')}
|
{t('save_config')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ import { UINT32_RANGE } from '../../../helpers/constants';
|
|||||||
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
|
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
|
||||||
import { DhcpFormValues } from '.';
|
import { DhcpFormValues } from '.';
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
v6?: {
|
|
||||||
range_start?: string;
|
|
||||||
range_end?: string;
|
|
||||||
lease_duration?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormDHCPv6Props = {
|
type FormDHCPv6Props = {
|
||||||
processingConfig?: boolean;
|
processingConfig?: boolean;
|
||||||
ipv6placeholders?: {
|
ipv6placeholders?: {
|
||||||
@@ -40,18 +32,12 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
const formValues = watch('v6');
|
const formValues = watch('v6');
|
||||||
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
||||||
|
|
||||||
const handleFormSubmit = async (data: FormValues) => {
|
|
||||||
if (onSubmit) {
|
|
||||||
await onSubmit(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDisabled = useMemo(() => {
|
const isDisabled = useMemo(() => {
|
||||||
return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig;
|
return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig;
|
||||||
}, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]);
|
}, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
@@ -62,6 +48,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<input
|
<input
|
||||||
|
data-testid="v6_range_start"
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv6placeholders?.range_start || '')}
|
placeholder={t(ipv6placeholders?.range_start || '')}
|
||||||
@@ -70,7 +57,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
validate: {
|
validate: {
|
||||||
ipv6: validateIpv6,
|
ipv6: validateIpv6,
|
||||||
required: (value) =>
|
required: (value) =>
|
||||||
isEmptyConfig ? undefined : validateRequiredValue(value),
|
isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -83,6 +70,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<input
|
<input
|
||||||
|
data-testid="v6_range_end"
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv6placeholders?.range_end || '')}
|
placeholder={t(ipv6placeholders?.range_end || '')}
|
||||||
@@ -91,7 +79,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
validate: {
|
validate: {
|
||||||
ipv6: validateIpv6,
|
ipv6: validateIpv6,
|
||||||
required: (value) =>
|
required: (value) =>
|
||||||
isEmptyConfig ? undefined : validateRequiredValue(value),
|
isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -110,6 +98,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
<div className="col-lg-6 form__group form__group--settings">
|
<div className="col-lg-6 form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_lease_title')}</label>
|
<label>{t('dhcp_form_lease_title')}</label>
|
||||||
<input
|
<input
|
||||||
|
data-testid="v6_lease_duration"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t(ipv6placeholders?.lease_duration || '')}
|
placeholder={t(ipv6placeholders?.lease_duration || '')}
|
||||||
@@ -119,7 +108,8 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
{...register('v6.lease_duration', {
|
{...register('v6.lease_duration', {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
validate: {
|
validate: {
|
||||||
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
required: (value) =>
|
||||||
|
isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -130,7 +120,11 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button type="submit" className="btn btn-success btn-standard" disabled={isDisabled}>
|
<button
|
||||||
|
data-testid="v6_save"
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={isDisabled}>
|
||||||
{t('save_config')}
|
{t('save_config')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ const Interfaces = () => {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="interface_name"
|
id="interface_name"
|
||||||
|
data-testid="interface_name"
|
||||||
className="form-control custom-select pl-4 col-md"
|
className="form-control custom-select pl-4 col-md"
|
||||||
disabled={enabled}
|
disabled={enabled}
|
||||||
{...register('interface_name', {
|
{...register('interface_name', {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const Dhcp = () => {
|
|||||||
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
|
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
|
||||||
|
|
||||||
const methods = useForm<DhcpFormValues>({
|
const methods = useForm<DhcpFormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
v4: v4 || DEFAULT_V4_VALUES,
|
v4: v4 || DEFAULT_V4_VALUES,
|
||||||
v6: v6 || DEFAULT_V6_VALUES,
|
v6: v6 || DEFAULT_V6_VALUES,
|
||||||
@@ -127,8 +127,14 @@ const Dhcp = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (v4 || v6 || interfaceName) {
|
if (v4 || v6 || interfaceName) {
|
||||||
reset({
|
reset({
|
||||||
v4: v4 || DEFAULT_V4_VALUES,
|
v4: {
|
||||||
v6: v6 || DEFAULT_V6_VALUES,
|
...DEFAULT_V4_VALUES,
|
||||||
|
...v4,
|
||||||
|
},
|
||||||
|
v6: {
|
||||||
|
...DEFAULT_V6_VALUES,
|
||||||
|
...v6,
|
||||||
|
},
|
||||||
interface_name: interfaceName || '',
|
interface_name: interfaceName || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
|||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
allowed_clients: initialValues?.allowed_clients || '',
|
allowed_clients: initialValues?.allowed_clients || '',
|
||||||
disallowed_clients: initialValues?.disallowed_clients || '',
|
disallowed_clients: initialValues?.disallowed_clients || '',
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
|
|||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
cache_size: initialValues?.cache_size || 0,
|
cache_size: initialValues?.cache_size || 0,
|
||||||
cache_ttl_min: initialValues?.cache_ttl_min || 0,
|
cache_ttl_min: initialValues?.cache_ttl_min || 0,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
|||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting, isDirty },
|
formState: { errors, isSubmitting, isDirty },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
|
|||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
upstream_dns: initialValues?.upstream_dns || '',
|
upstream_dns: initialValues?.upstream_dns || '',
|
||||||
upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Checkbox } from '../../ui/Controls/Checkbox';
|
|||||||
import { Radio } from '../../ui/Controls/Radio';
|
import { Radio } from '../../ui/Controls/Radio';
|
||||||
import { Input } from '../../ui/Controls/Input';
|
import { Input } from '../../ui/Controls/Input';
|
||||||
import { Textarea } from '../../ui/Controls/Textarea';
|
import { Textarea } from '../../ui/Controls/Textarea';
|
||||||
|
import { EncryptionData } from '../../../initialState';
|
||||||
|
|
||||||
const certificateSourceOptions = [
|
const certificateSourceOptions = [
|
||||||
{
|
{
|
||||||
@@ -71,7 +72,7 @@ const validationMessage = (warningValidation: string, isWarning: boolean) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormValues = {
|
export type EncryptionFormValues = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
serve_plain_dns: boolean;
|
serve_plain_dns: boolean;
|
||||||
server_name: string;
|
server_name: string;
|
||||||
@@ -89,7 +90,7 @@ export type FormValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialValues: FormValues;
|
initialValues: EncryptionFormValues;
|
||||||
processingConfig: boolean;
|
processingConfig: boolean;
|
||||||
processingValidate: boolean;
|
processingValidate: boolean;
|
||||||
status_key?: string;
|
status_key?: string;
|
||||||
@@ -103,10 +104,10 @@ type Props = {
|
|||||||
key_type?: string;
|
key_type?: string;
|
||||||
issuer?: string;
|
issuer?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
onSubmit: (...args: unknown[]) => void;
|
onSubmit: (values: EncryptionFormValues) => void;
|
||||||
onChange: (...args: unknown[]) => void;
|
debouncedConfigValidation: (values: EncryptionFormValues) => void;
|
||||||
setTlsConfig: (...args: unknown[]) => void;
|
setTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
validateTlsConfig: (...args: unknown[]) => void;
|
validateTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
@@ -141,11 +142,12 @@ export const Form = ({
|
|||||||
subject,
|
subject,
|
||||||
warning_validation,
|
warning_validation,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
debouncedConfigValidation,
|
||||||
setTlsConfig,
|
setTlsConfig,
|
||||||
validateTlsConfig,
|
validateTlsConfig,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const previousValuesRef = useRef<FormValues>(initialValues);
|
const previousValuesRef = useRef<EncryptionFormValues>(initialValues);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -156,31 +158,35 @@ export const Form = ({
|
|||||||
setError,
|
setError,
|
||||||
getValues,
|
getValues,
|
||||||
formState: { isSubmitting, isValid },
|
formState: { isSubmitting, isValid },
|
||||||
} = useForm<FormValues>({
|
} = useForm<EncryptionFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...initialValues,
|
...initialValues,
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
},
|
},
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchedValues = watch();
|
const watchedValues = watch();
|
||||||
|
|
||||||
const isEnabled = watch('enabled');
|
const {
|
||||||
const servePlainDns = watch('serve_plain_dns');
|
enabled: isEnabled,
|
||||||
const certificateChain = watch('certificate_chain');
|
serve_plain_dns: servePlainDns,
|
||||||
const privateKey = watch('private_key');
|
certificate_chain: certificateChain,
|
||||||
const certificatePath = watch('certificate_path');
|
private_key: privateKey,
|
||||||
const privateKeyPath = watch('private_key_path');
|
certificate_path: certificatePath,
|
||||||
const certificateSource = watch('certificate_source');
|
private_key_path: privateKeyPath,
|
||||||
const privateKeySaved = watch('private_key_saved');
|
certificate_source: certificateSource,
|
||||||
const privateKeySource = watch('key_source');
|
private_key_saved: privateKeySaved,
|
||||||
|
key_source: privateKeySource,
|
||||||
|
} = watchedValues;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previousValues = previousValuesRef.current;
|
const previousValues = previousValuesRef.current;
|
||||||
|
|
||||||
if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) {
|
if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) {
|
||||||
// TODO onChange TLS config validation
|
// TODO(ik) onChange TLS config validation
|
||||||
|
console.log('debouncedConfigValidation');
|
||||||
|
debouncedConfigValidation(watchedValues);
|
||||||
previousValuesRef.current = watchedValues;
|
previousValuesRef.current = watchedValues;
|
||||||
}
|
}
|
||||||
}, [watchedValues]);
|
}, [watchedValues]);
|
||||||
@@ -203,7 +209,7 @@ export const Form = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validatePorts = (values: FormValues) => {
|
const validatePorts = (values: EncryptionFormValues) => {
|
||||||
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
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) {
|
||||||
@@ -216,12 +222,12 @@ export const Form = ({
|
|||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = (data: FormValues) => {
|
const onFormSubmit = (data: EncryptionFormValues) => {
|
||||||
const validationErrors = validatePorts(data);
|
const validationErrors = validatePorts(data);
|
||||||
|
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
Object.entries(validationErrors).forEach(([field, message]) => {
|
Object.entries(validationErrors).forEach(([field, message]) => {
|
||||||
setError(field as keyof FormValues, { type: 'manual', message });
|
setError(field as keyof EncryptionFormValues, { type: 'manual', message });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
||||||
|
|
||||||
import { Form, FormValues } from './Form';
|
import { EncryptionFormValues, Form } 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';
|
||||||
@@ -11,8 +11,8 @@ import { EncryptionData } from '../../../initialState';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
encryption: EncryptionData;
|
encryption: EncryptionData;
|
||||||
setTlsConfig: (values: EncryptionData) => void;
|
setTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
validateTlsConfig: (values: EncryptionData) => void;
|
validateTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
|
export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
|
||||||
@@ -24,7 +24,7 @@ export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Prop
|
|||||||
}
|
}
|
||||||
}, [encryption, validateTlsConfig]);
|
}, [encryption, validateTlsConfig]);
|
||||||
|
|
||||||
const getInitialValues = useCallback((data: any): FormValues => {
|
const getInitialValues = useCallback((data: any): EncryptionFormValues => {
|
||||||
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;
|
||||||
@@ -67,15 +67,16 @@ export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Prop
|
|||||||
[getSubmitValues, setTlsConfig],
|
[getSubmitValues, setTlsConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const debouncedConfigValidation = useCallback(
|
||||||
debounce((values) => {
|
debounce((values) => {
|
||||||
const submitValues = getSubmitValues(values);
|
const submitValues = getSubmitValues(values);
|
||||||
|
|
||||||
if (submitValues.enabled) {
|
if (submitValues.enabled) {
|
||||||
|
console.log('validateTlsConfig');
|
||||||
validateTlsConfig(submitValues);
|
validateTlsConfig(submitValues);
|
||||||
}
|
}
|
||||||
}, DEBOUNCE_TIMEOUT),
|
}, DEBOUNCE_TIMEOUT),
|
||||||
[getSubmitValues, validateTlsConfig],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialValues = getInitialValues(encryption);
|
const initialValues = getInitialValues(encryption);
|
||||||
@@ -94,7 +95,7 @@ export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Prop
|
|||||||
<Form
|
<Form
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
onChange={handleChange}
|
debouncedConfigValidation={debouncedConfigValidation}
|
||||||
setTlsConfig={setTlsConfig}
|
setTlsConfig={setTlsConfig}
|
||||||
validateTlsConfig={validateTlsConfig}
|
validateTlsConfig={validateTlsConfig}
|
||||||
{...encryption}
|
{...encryption}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
|
|||||||
const prevFormValuesRef = useRef<FormValues>(initialValues);
|
const prevFormValuesRef = useRef<FormValues>(initialValues);
|
||||||
|
|
||||||
const { register, watch, control } = useForm({
|
const { register, watch, control } = useForm({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
control,
|
control,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enabled: initialValues.enabled || false,
|
enabled: initialValues.enabled || false,
|
||||||
anonymize_client_ip: initialValues.anonymize_client_ip || false,
|
anonymize_client_ip: initialValues.anonymize_client_ip || false,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
|||||||
control,
|
control,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...defaultFormValues,
|
...defaultFormValues,
|
||||||
...initialValues,
|
...initialValues,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
|
|||||||
control,
|
control,
|
||||||
formState: { isValid },
|
formState: { isValid },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...defaultFormValues,
|
...defaultFormValues,
|
||||||
...initialValues,
|
...initialValues,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const validateGatewaySubnetMask = (_: any, allValues: any) => {
|
|||||||
* @param allValues
|
* @param allValues
|
||||||
*/
|
*/
|
||||||
export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
||||||
if (!allValues || !allValues.v4 || !value) {
|
if (!allValues || !allValues.v4 || !value || !allValues.gateway_ip || !allValues.subnet_mask) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const Auth = ({ onAuthSubmit }: Props) => {
|
|||||||
control,
|
control,
|
||||||
formState: { isDirty, isValid },
|
formState: { isDirty, isValid },
|
||||||
} = useForm<AuthFormValues>({
|
} = useForm<AuthFormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
|
|||||||
formState: { isValid },
|
formState: { isValid },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchFields = watch();
|
const watchFields = watch();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
|||||||
control,
|
control,
|
||||||
formState: { isValid },
|
formState: { isValid },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -39,6 +39,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="username"
|
||||||
type="text"
|
type="text"
|
||||||
label={t('username_label')}
|
label={t('username_label')}
|
||||||
placeholder={t('username_placeholder')}
|
placeholder={t('username_placeholder')}
|
||||||
@@ -59,6 +60,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
|
data-testid="password"
|
||||||
type="password"
|
type="password"
|
||||||
label={t('username_label')}
|
label={t('username_label')}
|
||||||
placeholder={t('password_placeholder')}
|
placeholder={t('password_placeholder')}
|
||||||
@@ -71,7 +73,11 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<button type="submit" className="btn btn-success btn-block" disabled={processing || !isValid}>
|
<button
|
||||||
|
data-testid="sign_in"
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-block"
|
||||||
|
disabled={processing || !isValid}>
|
||||||
{t('sign_in')}
|
{t('sign_in')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
3
client/tests/constants.ts
Normal file
3
client/tests/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const ADMIN_USERNAME = 'admin';
|
||||||
|
export const ADMIN_PASSWORD = 'superpassword';
|
||||||
|
export const PORT = 3000;
|
||||||
62
client/tests/e2e/dhcp.spec.ts
Normal file
62
client/tests/e2e/dhcp.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
|
||||||
|
|
||||||
|
const INTERFACE_NAME = 'en0';
|
||||||
|
const RANGE_START = '192.168.1.100';
|
||||||
|
const RANGE_END = '192.168.1.200';
|
||||||
|
const SUBNET_MASK = '255.255.255.0';
|
||||||
|
const LEASE_TIME = '86400';
|
||||||
|
|
||||||
|
test.describe('DHCP Configuration', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login.html');
|
||||||
|
await page.getByTestId('username').click();
|
||||||
|
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||||
|
await page.getByTestId('password').click();
|
||||||
|
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||||
|
await page.getByTestId('sign_in').click();
|
||||||
|
await page.getByText('Settings', { exact: true }).click();
|
||||||
|
await page.goto(`/#dhcp`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select the correct DHCP interface', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
expect(await page.locator('select[name="interface_name"]').inputValue()).toBe(INTERFACE_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should configure DHCP IPv4 settings correctly', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
await page.getByTestId('v4_gateway_ip').click();
|
||||||
|
await page.getByTestId('v4_gateway_ip').fill('192.168.1.99');
|
||||||
|
await page.getByTestId('v4_subnet_mask').click();
|
||||||
|
await page.getByTestId('v4_subnet_mask').fill(SUBNET_MASK);
|
||||||
|
await page.getByTestId('v4_range_start').click();
|
||||||
|
await page.getByTestId('v4_range_start').fill(RANGE_START);
|
||||||
|
await page.getByTestId('v4_range_end').click();
|
||||||
|
await page.getByTestId('v4_range_end').fill(RANGE_END);
|
||||||
|
await page.getByTestId('v4_lease_duration').click();
|
||||||
|
await page.getByTestId('v4_lease_duration').fill(LEASE_TIME);
|
||||||
|
await page.getByTestId('v4_save').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error for invalid DHCP IPv4 range', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
await page.getByTestId('v4_range_start').click();
|
||||||
|
await page.getByTestId('v4_range_start').fill(RANGE_END);
|
||||||
|
await page.getByTestId('v4_range_end').click();
|
||||||
|
await page.getByTestId('v4_range_end').fill(RANGE_START);
|
||||||
|
await page.locator('.col-12').first().click();
|
||||||
|
await page.getByText('Must be greater than range').click();
|
||||||
|
|
||||||
|
expect(await page.getByText('Must be greater than range').isVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error for invalid DHCP IPv4 address', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
await page.getByTestId('v4_gateway_ip').click();
|
||||||
|
await page.getByTestId('v4_gateway_ip').fill('192.168.1.200s');
|
||||||
|
await page.getByText('Invalid IPv4 address').click();
|
||||||
|
|
||||||
|
expect(await page.getByText('Invalid IPv4 address').isVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
client/tests/e2e/login.spec.ts
Normal file
13
client/tests/e2e/login.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { test } from '@playwright/test';
|
||||||
|
import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
|
||||||
|
|
||||||
|
test.describe('Login', () => {
|
||||||
|
test('should successfully log in with valid credentials', async ({ page }) => {
|
||||||
|
await page.goto(`/login.html`);
|
||||||
|
await page.getByTestId('username').click();
|
||||||
|
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||||
|
await page.getByTestId('password').click();
|
||||||
|
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||||
|
await page.getByTestId('sign_in').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user