fix form validation mode

This commit is contained in:
Ildar Kamalov
2025-01-24 14:49:12 +03:00
parent 254b25a026
commit 681cdb023e
27 changed files with 178 additions and 83 deletions

2
client/package.json vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ export const Form = ({
...defaultFormValues, ...defaultFormValues,
...initialValues, ...initialValues,
}, },
mode: 'onChange', mode: 'onBlur',
}); });
const { const {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export const ADMIN_USERNAME = 'admin';
export const ADMIN_PASSWORD = 'superpassword';
export const PORT = 3000;

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

View 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();
});
});