Pull request 2322: ADG-9415

Merge in DNS/adguard-home from ADG-9415 to master

Squashed commit of the following:

commit 76bf99499a
Merge: 29529970a 0389515ee
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Feb 26 18:31:41 2025 +0300

    Merge branch 'master' into ADG-9415

commit 29529970a3
Merge: b49790daf 782a1a982
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 24 15:44:38 2025 +0300

    Merge branch 'master' into ADG-9415

commit b49790daf8
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 24 15:30:18 2025 +0300

    fix default lease duration value

commit cb307472ec
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 24 10:35:26 2025 +0300

    fix default response status

commit 115e743e1a
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 24 10:32:46 2025 +0300

    fix upstream description

commit 26b0eddaca
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 18 17:40:41 2025 +0300

    use const for test config file

commit 58faa7c537
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 18 17:31:04 2025 +0300

    fix install config

commit 0a3346d911
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 17 15:25:23 2025 +0300

    fix install check config

commit 17c4c26ea8
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 17:18:20 2025 +0300

    fix query log

commit 14a2685ae3
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 15:52:36 2025 +0300

    fix dhcp initial values

commit e7a8db7afd
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 14:37:24 2025 +0300

    fix encryption form values

commit 1c8917f7ac
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 14:07:29 2025 +0300

    fix blocked services submit

commit 4dfa536cea
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 13:50:47 2025 +0300

    dns config ip validation

commit 4fee83fe13
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Feb 12 17:49:54 2025 +0300

    add playwright warning

commit 8c2f36e7a6
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 18:36:18 2025 +0300

    fix config file name

commit 83db5f33dc
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 16:16:43 2025 +0300

    temp config file

commit 9080c1620f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 15:01:46 2025 +0300

    update readme

commit ee1520307f
Merge: fd12e33c0 2fe2d254b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 14:44:06 2025 +0300

    Merge branch 'master' into ADG-9415

commit fd12e33c06
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Feb 10 10:29:43 2025 +0100

    added typecheck on build, fixed eslint

commit b3849eebc4
Merge: 225167a8b 9bf3ee128
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Feb 10 09:43:32 2025 +0100

    Merge branch 'ADG-9415' of https://bit.int.agrd.dev/scm/dns/adguard-home into ADG-9415

... and 94 more commits
This commit is contained in:
Ildar Kamalov
2025-02-26 19:37:52 +03:00
parent 0389515ee3
commit 8b2ab8ea87
102 changed files with 7075 additions and 10256 deletions

View File

@@ -1,28 +1,22 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import React, { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { renderInputField, toNumber } from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import { UINT32_RANGE } from '../../../helpers/constants';
import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateIpv4,
validateIpv4RangeEnd,
validateNotInRange,
validateRequiredValue,
} from '../../../helpers/validators';
import { RootState } from '../../../initialState';
import { DhcpFormValues } from '.';
import { Input } from '../../ui/Controls/Input';
import { toNumber } from '../../../helpers/form';
interface FormDHCPv4Props {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
initialValues: { v4?: any };
type FormDHCPv4Props = {
processingConfig?: boolean;
change: (field: string, value: any) => void;
reset: () => void;
ipv4placeholders?: {
gateway_ip: string;
subnet_mask: string;
@@ -30,127 +24,179 @@ interface FormDHCPv4Props {
range_end: string;
lease_duration: string;
};
}
interfaces: any;
onSubmit?: (data: DhcpFormValues) => void;
};
const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholders }: FormDHCPv4Props) => {
const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => {
const { t } = useTranslation();
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const {
handleSubmit,
formState: { errors, isSubmitting },
control,
watch,
} = useFormContext<DhcpFormValues>();
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const interfaceName = watch('interface_name');
const isInterfaceIncludesIpv4 = interfaces?.[interfaceName]?.ipv4_addresses;
const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const formValues = watch('v4');
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
const hasV4Errors = errors.v4 && Object.keys(errors.v4).length > 0;
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const invalid =
dhcp?.syncErrors ||
interfaces?.syncErrors ||
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
submitting ||
processingConfig;
const validateRequired = useCallback(
(value) => {
if (isEmptyConfig) {
return undefined;
}
return validateRequiredValue(value);
},
[isEmptyConfig],
);
const isDisabled = useMemo(() => {
return isSubmitting || hasV4Errors || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig;
}, [isSubmitting, hasV4Errors, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]);
return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
<Controller
name="v4.gateway_ip"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.gateway_ip)}
validate={[validateIpv4, validateRequired, validateNotInRange]}
disabled={!isInterfaceIncludesIpv4}
control={control}
rules={{
validate: {
ipv4: validateIpv4,
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
notInRange: validateNotInRange,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="v4_gateway_ip"
label={t('dhcp_form_gateway_input')}
placeholder={t(ipv4placeholders.gateway_ip)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv4}
/>
)}
/>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
<Controller
name="v4.subnet_mask"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.subnet_mask)}
validate={[validateRequired, validateGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
control={control}
rules={{
validate: {
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
subnet: validateGatewaySubnetMask,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="v4_subnet_mask"
label={t('dhcp_form_subnet_input')}
placeholder={t(ipv4placeholders.subnet_mask)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv4}
/>
)}
/>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="form__group mb-0">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
<Controller
name="v4.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_start)}
validate={[validateIpv4, validateIpForGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
control={control}
rules={{
validate: {
ipv4: validateIpv4,
gateway: validateIpForGatewaySubnetMask,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="v4_range_start"
placeholder={t(ipv4placeholders.range_start)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv4}
/>
)}
/>
</div>
<div className="col">
<Field
<Controller
name="v4.range_end"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_end)}
validate={[validateIpv4, validateIpv4RangeEnd, validateIpForGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
control={control}
rules={{
validate: {
ipv4: validateIpv4,
rangeEnd: validateIpv4RangeEnd,
gateway: validateIpForGatewaySubnetMask,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="v4_range_end"
placeholder={t(ipv4placeholders.range_end)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv4}
/>
)}
/>
</div>
</div>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
<Controller
name="v4.lease_duration"
component={renderInputField}
type="number"
className="form-control"
placeholder={t(ipv4placeholders.lease_duration)}
validate={validateRequired}
normalize={toNumber}
min={1}
max={UINT32_RANGE.MAX}
disabled={!isInterfaceIncludesIpv4}
control={control}
rules={{
validate: {
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="number"
data-testid="v4_lease_duration"
label={t('dhcp_form_lease_title')}
placeholder={t(ipv4placeholders.lease_duration)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv4}
min={1}
max={UINT32_RANGE.MAX}
value={field.value ?? ''}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
</div>
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
<button
data-testid="v4_save"
type="submit"
className="btn btn-success btn-standard"
disabled={isDisabled}>
{t('save_config')}
</button>
</div>
@@ -158,9 +204,4 @@ const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholde
);
};
export default reduxForm<
Record<string, any>,
Omit<FormDHCPv4Props, 'submitting' | 'handleSubmit' | 'reset' | 'change'>
>({
form: FORM_NAME.DHCPv4,
})(FormDHCPv4);
export default FormDHCPv4;

View File

@@ -1,93 +1,92 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import React, { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { renderInputField, toNumber } from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import { UINT32_RANGE } from '../../../helpers/constants';
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
import { RootState } from '../../../initialState';
import { DhcpFormValues } from '.';
import { Input } from '../../ui/Controls/Input';
import { toNumber } from '../../../helpers/form';
interface FormDHCPv6Props {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
initialValues: {
v6?: any;
};
change: (field: string, value: any) => void;
reset: () => void;
type FormDHCPv6Props = {
processingConfig?: boolean;
ipv6placeholders?: {
range_start: string;
range_end: string;
lease_duration: string;
};
}
interfaces: any;
onSubmit?: (data: DhcpFormValues) => Promise<void> | void;
};
const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholders }: FormDHCPv6Props) => {
const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }: FormDHCPv6Props) => {
const { t } = useTranslation();
const {
handleSubmit,
formState: { isSubmitting, isValid },
control,
watch,
} = useFormContext<DhcpFormValues>();
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv6], shallowEqual);
const interfaceName = watch('interface_name');
const isInterfaceIncludesIpv6 = interfaces?.[interfaceName]?.ipv6_addresses;
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const formValues = watch('v6');
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
const isInterfaceIncludesIpv6 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses,
);
const isEmptyConfig = !Object.values(dhcp?.values?.v6 ?? {}).some(Boolean);
const invalid =
dhcp?.syncErrors ||
interfaces?.syncErrors ||
!isInterfaceIncludesIpv6 ||
isEmptyConfig ||
submitting ||
processingConfig;
const validateRequired = useCallback(
(value) => {
if (isEmptyConfig) {
return undefined;
}
return validateRequiredValue(value);
},
[isEmptyConfig],
);
const isDisabled = useMemo(() => {
return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig;
}, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]);
return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="form__group mb-0">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
<Controller
name="v6.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv6placeholders.range_start)}
validate={[validateIpv6, validateRequired]}
disabled={!isInterfaceIncludesIpv6}
control={control}
rules={{
validate: isInterfaceIncludesIpv6
? {
ipv6: validateIpv6,
required: validateRequiredValue,
}
: undefined,
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="v6_range_start"
placeholder={t(ipv6placeholders.range_start)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv6}
/>
)}
/>
</div>
<div className="col">
<Field
<Controller
name="v6.range_end"
component="input"
type="text"
className="form-control disabled cursor--not-allowed"
placeholder={t(ipv6placeholders.range_end)}
value={t(ipv6placeholders.range_end)}
disabled
control={control}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="v6_range_end"
placeholder={t(ipv6placeholders.range_end)}
error={fieldState.error?.message}
disabled
/>
)}
/>
</div>
</div>
@@ -97,25 +96,43 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
<div className="row">
<div className="col-lg-6 form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
<Controller
name="v6.lease_duration"
component={renderInputField}
type="number"
className="form-control"
placeholder={t(ipv6placeholders.lease_duration)}
validate={validateRequired}
normalizeOnBlur={toNumber}
min={1}
max={UINT32_RANGE.MAX}
disabled={!isInterfaceIncludesIpv6}
control={control}
rules={{
validate: isInterfaceIncludesIpv6
? {
required: validateRequiredValue,
}
: undefined,
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="number"
data-testid="v6_lease_duration"
label={t('dhcp_form_lease_title')}
placeholder={t(ipv6placeholders.lease_duration)}
error={fieldState.error?.message}
disabled={!isInterfaceIncludesIpv6}
min={1}
max={UINT32_RANGE.MAX}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
<button
data-testid="v6_save"
type="submit"
className="btn btn-success btn-standard"
disabled={isDisabled}>
{t('save_config')}
</button>
</div>
@@ -123,9 +140,4 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
);
};
export default reduxForm<
Record<string, any>,
Omit<FormDHCPv6Props, 'handleSubmit' | 'change' | 'submitting' | 'reset'>
>({
form: FORM_NAME.DHCPv6,
})(FormDHCPv6);
export default FormDHCPv6;

View File

@@ -1,13 +1,11 @@
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { useSelector } from 'react-redux';
import { useFormContext } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { renderSelectField } from '../../../helpers/form';
import { validateRequiredValue } from '../../../helpers/validators';
import { FORM_NAME } from '../../../helpers/constants';
import { RootState } from '../../../initialState';
import { DhcpFormValues } from '.';
const renderInterfaces = (interfaces: any) =>
Object.keys(interfaces).map((item) => {
@@ -47,13 +45,13 @@ const getInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: any)
},
];
interface renderInterfaceValuesProps {
interface RenderInterfaceValuesProps {
gateway_ip: string;
hardware_address: string;
ip_addresses: string[];
}
const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: renderInterfaceValuesProps) => (
const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: RenderInterfaceValuesProps) => (
<div className="d-flex align-items-end dhcp__interfaces-info">
<ul className="list-unstyled m-0">
{getInterfaceValues({
@@ -77,11 +75,15 @@ const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: r
const Interfaces = () => {
const { t } = useTranslation();
const {
register,
watch,
formState: { errors },
} = useFormContext<DhcpFormValues>();
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp, shallowEqual);
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp);
const interface_name =
useSelector((store: RootState) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
const interface_name = watch('interface_name');
if (processingInterfaces || !interfaces) {
return null;
@@ -92,27 +94,34 @@ const Interfaces = () => {
return (
<div className="row dhcp__interfaces">
<div className="col col__dhcp">
<Field
name="interface_name"
component={renderSelectField}
<label htmlFor="interface_name" className="form__label">
{t('dhcp_interface_select')}
</label>
<select
id="interface_name"
data-testid="interface_name"
className="form-control custom-select pl-4 col-md"
validate={[validateRequiredValue]}
label="dhcp_interface_select">
disabled={enabled}
{...register('interface_name', {
validate: validateRequiredValue,
})}>
<option value="" disabled={enabled}>
{t('dhcp_interface_select')}
</option>
{renderInterfaces(interfaces)}
</Field>
</select>
{errors.interface_name && (
<div className="form__message form__message--error">{t(errors.interface_name.message)}</div>
)}
</div>
{interfaceValue && renderInterfaceValues({
gateway_ip: interfaceValue.gateway_ip,
hardware_address: interfaceValue.hardware_address,
ip_addresses: interfaceValue.ip_addresses
})}
{interfaceValue &&
renderInterfaceValues({
gateway_ip: interfaceValue.gateway_ip,
hardware_address: interfaceValue.hardware_address,
ip_addresses: interfaceValue.ip_addresses,
})}
</div>
);
};
export default reduxForm({
form: FORM_NAME.DHCP_INTERFACES,
})(Interfaces);
export default Interfaces;

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { useForm, Controller } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { renderInputField, normalizeMac } from '../../../../helpers/form';
import { normalizeMac } from '../../../../helpers/form';
import {
validateIpv4,
validateMac,
@@ -12,12 +11,12 @@ import {
validateIpv4InCidr,
validateIpGateway,
} from '../../../../helpers/validators';
import { FORM_NAME } from '../../../../helpers/constants';
import { toggleLeaseModal } from '../../../../actions';
import { RootState } from '../../../../initialState';
import { Input } from '../../../ui/Controls/Input';
interface FormStaticLeaseProps {
type Props = {
initialValues?: {
mac?: string;
ip?: string;
@@ -25,63 +24,91 @@ interface FormStaticLeaseProps {
cidr?: string;
gatewayIp?: string;
};
pristine: boolean;
handleSubmit: (...args: unknown[]) => string;
reset: () => void;
submitting: boolean;
processingAdding?: boolean;
cidr?: string;
isEdit?: boolean;
}
onSubmit: (data: any) => void;
};
const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cidr, isEdit }: FormStaticLeaseProps) => {
export const Form = ({ initialValues, processingAdding, cidr, isEdit, onSubmit }: Props) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);
const {
handleSubmit,
control,
reset,
formState: { isSubmitting, isDirty },
} = useForm({
defaultValues: initialValues,
mode: 'onBlur',
});
const onClick = () => {
reset();
dispatch(toggleLeaseModal());
};
return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="modal-body">
<div className="form__group">
<Field
id="mac"
<Controller
name="mac"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_enter_mac')}
normalize={normalizeMac}
validate={[validateRequiredValue, validateMac]}
disabled={isEdit}
control={control}
rules={{ validate: { required: validateRequiredValue, mac: validateMac } }}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="static_lease_mac"
placeholder={t('form_enter_mac')}
disabled={isEdit}
error={fieldState.error?.message}
onChange={(e) => field.onChange(normalizeMac(e.target.value))}
/>
)}
/>
</div>
<div className="form__group">
<Field
id="ip"
<Controller
name="ip"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_enter_subnet_ip', { cidr })}
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr, validateIpGateway]}
control={control}
rules={{
validate: {
required: validateRequiredValue,
ipv4: validateIpv4,
inCidr: validateIpv4InCidr,
gateway: validateIpGateway,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="static_lease_ip"
error={fieldState.error?.message}
placeholder={t('form_enter_subnet_ip', { cidr })}
/>
)}
/>
</div>
<div className="form__group">
<Field
id="hostname"
<Controller
name="hostname"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_enter_hostname')}
control={control}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="static_lease_hostname"
error={fieldState.error?.message}
placeholder={t('form_enter_hostname')}
/>
)}
/>
</div>
</div>
@@ -90,16 +117,18 @@ const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cid
<div className="btn-list">
<button
type="button"
data-testid="static_lease_cancel"
className="btn btn-secondary btn-standard"
disabled={submitting}
disabled={isSubmitting}
onClick={onClick}>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
data-testid="static_lease_save"
className="btn btn-success btn-standard"
disabled={submitting || processingAdding || (pristine && !dynamicLease)}>
disabled={isSubmitting || processingAdding || (!isDirty && !dynamicLease)}>
<Trans>save_btn</Trans>
</button>
</div>
@@ -107,8 +136,3 @@ const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cid
</form>
);
};
export default reduxForm<
Record<string, any>,
Omit<FormStaticLeaseProps, 'submitting' | 'handleSubmit' | 'reset' | 'pristine'>
>({ form: FORM_NAME.LEASE })(Form);

View File

@@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import { Form } from './Form';
import { toggleLeaseModal } from '../../../../actions';
import { MODAL_TYPE } from '../../../../helpers/constants';

View File

@@ -4,8 +4,8 @@ import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import { destroy } from 'redux-form';
import { DHCP_DESCRIPTION_PLACEHOLDERS, DHCP_FORM_NAMES, STATUS_RESPONSE, FORM_NAME } from '../../../helpers/constants';
import { FormProvider, useForm } from 'react-hook-form';
import { DHCP_DESCRIPTION_PLACEHOLDERS, STATUS_RESPONSE } from '../../../helpers/constants';
import Leases from './Leases';
@@ -40,6 +40,55 @@ import {
import './index.css';
import { RootState } from '../../../initialState';
type IPv4FormValues = {
gateway_ip?: string;
subnet_mask?: string;
range_start?: string;
range_end?: string;
lease_duration?: number;
}
type IPv6FormValues = {
range_start?: string;
range_end?: string;
lease_duration?: number;
}
const getDefaultV4Values = (v4: IPv4FormValues) => {
const emptyForm = Object.entries(v4).every(
([key, value]) => key === 'lease_duration' || value === ''
);
if (emptyForm) {
return {
...v4,
lease_duration: undefined,
}
}
return v4;
}
export type DhcpFormValues = {
v4?: IPv4FormValues;
v6?: IPv6FormValues;
interface_name?: string;
};
const DEFAULT_V4_VALUES = {
gateway_ip: '',
subnet_mask: '',
range_start: '',
range_end: '',
lease_duration: undefined,
};
const DEFAULT_V6_VALUES = {
range_start: '',
range_end: '',
lease_duration: undefined,
};
const Dhcp = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
@@ -65,12 +114,21 @@ 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 methods = useForm<DhcpFormValues>({
mode: 'onBlur',
defaultValues: {
v4: getDefaultV4Values(v4),
v6,
interface_name: interfaceName || '',
},
});
const { watch, reset } = methods;
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const interface_name = watch('interface_name');
const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const ipv4Config = watch('v4');
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
@@ -85,6 +143,22 @@ const Dhcp = () => {
}
}, [dhcp_available]);
useEffect(() => {
if (v4 || v6 || interfaceName) {
reset({
v4: {
...DEFAULT_V4_VALUES,
...getDefaultV4Values(v4),
},
v6: {
...DEFAULT_V6_VALUES,
...v6,
},
interface_name: interfaceName || '',
});
}
}, [v4, v6, interfaceName, reset]);
useEffect(() => {
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
@@ -103,13 +177,17 @@ const Dhcp = () => {
const clear = () => {
// eslint-disable-next-line no-alert
if (window.confirm(t('dhcp_reset'))) {
Object.values(DHCP_FORM_NAMES).forEach((formName: any) => dispatch(destroy(formName)));
reset({
v4: DEFAULT_V4_VALUES,
v6: DEFAULT_V6_VALUES,
interface_name: '',
});
dispatch(resetDhcp());
dispatch(getDhcpStatus());
}
};
const handleSubmit = (values: any) => {
const handleSubmit = (values: DhcpFormValues) => {
dispatch(
setDhcpConfig({
interface_name,
@@ -130,12 +208,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,
@@ -173,9 +246,6 @@ const Dhcp = () => {
const toggleModal = () => dispatch(toggleLeaseModal());
const initialV4 = enteredSomeV4Value ? v4 : {};
const initialV6 = enteredSomeV6Value ? v6 : {};
if (processing || processingInterfaces) {
return <Loading />;
}
@@ -196,19 +266,13 @@ const Dhcp = () => {
const toggleDhcpButton = getToggleDhcpButton();
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
const inputtedIPv4values = ipv4Config.gateway_ip && ipv4Config.subnet_mask;
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const isEmptyConfig = !Object.values(ipv4Config).some(Boolean);
const disabledLeasesButton = Boolean(
dhcp?.syncErrors ||
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
processingConfig ||
!inputtedIPv4values,
!isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,
);
const cidr = inputtedIPv4values
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
: '';
const cidr = inputtedIPv4values ? `${ipv4Config.gateway_ip}/${subnetMaskToBitMask(ipv4Config.subnet_mask)}` : '';
return (
<>
@@ -246,29 +310,30 @@ const Dhcp = () => {
</div>
)}
<Interfaces initialValues={{ interface_name: interfaceName }} />
<FormProvider {...methods}>
<Interfaces />
<Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv4
onSubmit={handleSubmit}
processingConfig={processingConfig}
ipv4placeholders={ipv4placeholders}
interfaces={interfaces}
/>
</div>
</Card>
<Card title={t('dhcp_ipv6_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv6
onSubmit={handleSubmit}
processingConfig={processingConfig}
ipv6placeholders={ipv6placeholders}
interfaces={interfaces}
/>
</div>
</Card>
</FormProvider>
<Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv4
onSubmit={handleSubmit}
initialValues={{ v4: initialV4 }}
processingConfig={processingConfig}
ipv4placeholders={ipv4placeholders}
/>
</div>
</Card>
<Card title={t('dhcp_ipv6_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv6
onSubmit={handleSubmit}
initialValues={{ v6: initialV6 }}
processingConfig={processingConfig}
ipv6placeholders={ipv6placeholders}
/>
</div>
</Card>
{enabled && (
<Card title={t('dhcp_leases')} bodyType="card-body box-body--settings">
<div className="row">
@@ -290,7 +355,7 @@ const Dhcp = () => {
processingDeleting={processingDeleting}
processingUpdating={processingUpdating}
cidr={cidr}
gatewayIp={dhcp?.values?.v4?.gateway_ip}
gatewayIp={ipv4Config.gateway_ip}
/>
<div className="btn-list mt-2">