add DHCP form

This commit is contained in:
Ildar Kamalov
2025-01-20 10:54:13 +03:00
parent 93890c2c6f
commit efd907216f
9 changed files with 179 additions and 261 deletions

View File

@@ -19,7 +19,6 @@ import {
CHECK_TIMEOUT, CHECK_TIMEOUT,
STATUS_RESPONSE, STATUS_RESPONSE,
SETTINGS_NAMES, SETTINGS_NAMES,
FORM_NAME,
MANUAL_UPDATE_LINK, MANUAL_UPDATE_LINK,
DISABLE_PROTECTION_TIMINGS, DISABLE_PROTECTION_TIMINGS,
} from '../helpers/constants'; } from '../helpers/constants';
@@ -511,16 +510,15 @@ export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS'); export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE'); export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
export const findActiveDhcp = (name: any) => async (dispatch: any, getState: any) => { export const findActiveDhcp = (selectedInterface: any) => async (dispatch: any, getState: any) => {
dispatch(findActiveDhcpRequest()); dispatch(findActiveDhcpRequest());
try { try {
const req = { const req = {
interface: name, interface: selectedInterface,
}; };
const activeDhcp = await apiClient.findActiveDhcp(req); const activeDhcp = await apiClient.findActiveDhcp(req);
dispatch(findActiveDhcpSuccess(activeDhcp)); dispatch(findActiveDhcpSuccess(activeDhcp));
const { check, interface_name, interfaces } = getState().dhcp; const { check, interface_name, interfaces } = getState().dhcp;
const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name;
const v4 = check?.v4 ?? { static_ip: {}, other_server: {} }; const v4 = check?.v4 ?? { static_ip: {}, other_server: {} };
const v6 = check?.v6 ?? { other_server: {} }; const v6 = check?.v6 ?? { other_server: {} };

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { UINT32_RANGE } from '../../../helpers/constants'; import { UINT32_RANGE } from '../../../helpers/constants';
import { import {
@@ -12,21 +11,10 @@ import {
validateNotInRange, validateNotInRange,
validateRequiredValue, validateRequiredValue,
} from '../../../helpers/validators'; } from '../../../helpers/validators';
import { RootState } from '../../../initialState'; import { DhcpFormValues } from '.';
type FormValues = {
v4?: {
gateway_ip?: string;
subnet_mask?: string;
range_start?: string;
range_end?: string;
lease_duration?: number;
};
}
type FormDHCPv4Props = { type FormDHCPv4Props = {
processingConfig?: boolean; processingConfig?: boolean;
initialValues?: FormValues;
ipv4placeholders?: { ipv4placeholders?: {
gateway_ip: string; gateway_ip: string;
subnet_mask: string; subnet_mask: string;
@@ -34,46 +22,28 @@ type FormDHCPv4Props = {
range_end: string; range_end: string;
lease_duration: string; lease_duration: string;
}; };
onSubmit?: (data: FormValues) => Promise<void> | void; interfaces: any;
} onSubmit?: (data: DhcpFormValues) => Promise<void> | void;
};
const FormDHCPv4 = ({ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => {
processingConfig,
initialValues,
ipv4placeholders,
onSubmit
}: FormDHCPv4Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const interfaces = useSelector((state: RootState) => state.form.DHCP_INTERFACES);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
watch, watch,
} = useForm<FormValues>({ } = useFormContext<DhcpFormValues>();
mode: 'onChange',
defaultValues: { const interfaceName = watch('interface_name');
v4: initialValues?.v4 || { const isInterfaceIncludesIpv4 = interfaces?.[interfaceName]?.ipv4_addresses;
gateway_ip: '',
subnet_mask: '',
range_start: '',
range_end: '',
lease_duration: 0,
},
},
});
const formValues = watch('v4'); const formValues = watch('v4');
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean); const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
const handleFormSubmit = async (data: FormValues) => { const handleFormSubmit = async (data: DhcpFormValues) => {
// TODO handle submit
if (onSubmit) { if (onSubmit) {
await onSubmit(data); await onSubmit(data);
} }
@@ -93,15 +63,13 @@ const FormDHCPv4 = ({
{...register('v4.gateway_ip', { {...register('v4.gateway_ip', {
validate: { validate: {
ipv4: validateIpv4, ipv4: validateIpv4,
required: (value) => isEmptyConfig ? undefined : validateRequiredValue(value), required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
notInRange: validateNotInRange, notInRange: validateNotInRange,
} },
})} })}
/> />
{errors.v4?.gateway_ip && ( {errors.v4?.gateway_ip && (
<div className="form__message form__message--error"> <div className="form__message form__message--error">{t(errors.v4.gateway_ip.message)}</div>
{t(errors.v4.gateway_ip.message)}
</div>
)} )}
</div> </div>
@@ -114,15 +82,13 @@ const FormDHCPv4 = ({
disabled={!isInterfaceIncludesIpv4} disabled={!isInterfaceIncludesIpv4}
{...register('v4.subnet_mask', { {...register('v4.subnet_mask', {
validate: { validate: {
required: (value) => isEmptyConfig ? undefined : validateRequiredValue(value), required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
subnet: validateGatewaySubnetMask, subnet: validateGatewaySubnetMask,
} },
})} })}
/> />
{errors.v4?.subnet_mask && ( {errors.v4?.subnet_mask && (
<div className="form__message form__message--error"> <div className="form__message form__message--error">{t(errors.v4.subnet_mask.message)}</div>
{t(errors.v4.subnet_mask.message)}
</div>
)} )}
</div> </div>
</div> </div>
@@ -144,7 +110,7 @@ const FormDHCPv4 = ({
validate: { validate: {
ipv4: validateIpv4, ipv4: validateIpv4,
gateway: validateIpForGatewaySubnetMask, gateway: validateIpForGatewaySubnetMask,
} },
})} })}
/> />
{errors.v4?.range_start && ( {errors.v4?.range_start && (
@@ -165,7 +131,7 @@ const FormDHCPv4 = ({
ipv4: validateIpv4, ipv4: validateIpv4,
rangeEnd: validateIpv4RangeEnd, rangeEnd: validateIpv4RangeEnd,
gateway: validateIpForGatewaySubnetMask, gateway: validateIpForGatewaySubnetMask,
} },
})} })}
/> />
{errors.v4?.range_end && ( {errors.v4?.range_end && (
@@ -189,8 +155,8 @@ const FormDHCPv4 = ({
{...register('v4.lease_duration', { {...register('v4.lease_duration', {
valueAsNumber: true, valueAsNumber: true,
validate: { validate: {
required: (value) => isEmptyConfig ? undefined : validateRequiredValue(value), required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
} },
})} })}
/> />
{errors.v4?.lease_duration && ( {errors.v4?.lease_duration && (
@@ -206,8 +172,13 @@ const FormDHCPv4 = ({
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard" className="btn btn-success btn-standard"
disabled={isSubmitting || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig || Object.keys(errors).length > 0} disabled={
> isSubmitting ||
processingConfig ||
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
Object.keys(errors).length > 0
}>
{t('save_config')} {t('save_config')}
</button> </button>
</div> </div>

View File

@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { UINT32_RANGE } from '../../../helpers/constants'; import { UINT32_RANGE } from '../../../helpers/constants';
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators'; import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
import { RootState } from '../../../initialState'; import { DhcpFormValues } from '.';
type FormValues = { type FormValues = {
v6?: { v6?: {
@@ -13,49 +12,30 @@ type FormValues = {
range_end?: string; range_end?: string;
lease_duration?: number; lease_duration?: number;
}; };
} };
type FormDHCPv6Props = { type FormDHCPv6Props = {
processingConfig?: boolean; processingConfig?: boolean;
initialValues?: FormValues;
ipv6placeholders?: { ipv6placeholders?: {
range_start: string; range_start: string;
range_end: string; range_end: string;
lease_duration: string; lease_duration: string;
}; };
onSubmit?: (data: FormValues) => Promise<void> | void; interfaces: any;
} onSubmit?: (data: DhcpFormValues) => Promise<void> | void;
};
const FormDHCPv6 = ({ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }: FormDHCPv6Props) => {
processingConfig,
initialValues,
ipv6placeholders,
onSubmit,
}: FormDHCPv6Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const interfaces = useSelector((state: RootState) => state.form.DHCP_INTERFACES);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv6 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses,
);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
watch, watch,
} = useForm<FormValues>({ } = useFormContext<DhcpFormValues>();
mode: 'onChange',
defaultValues: { const interfaceName = watch('interface_name');
v6: initialValues?.v6 || { const isInterfaceIncludesIpv6 = interfaces?.[interfaceName]?.ipv6_addresses;
range_start: '',
range_end: '',
lease_duration: 0,
},
},
});
const formValues = watch('v6'); const formValues = watch('v6');
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean); const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
@@ -85,8 +65,9 @@ const FormDHCPv6 = ({
{...register('v6.range_start', { {...register('v6.range_start', {
validate: { validate: {
ipv6: validateIpv6, ipv6: validateIpv6,
required: (value) => isEmptyConfig ? undefined : validateRequiredValue(value), required: (value) =>
} isEmptyConfig ? undefined : validateRequiredValue(value),
},
})} })}
/> />
{errors.v6?.range_start && ( {errors.v6?.range_start && (
@@ -105,8 +86,9 @@ const FormDHCPv6 = ({
{...register('v6.range_end', { {...register('v6.range_end', {
validate: { validate: {
ipv6: validateIpv6, ipv6: validateIpv6,
required: (value) => isEmptyConfig ? undefined : validateRequiredValue(value), required: (value) =>
} isEmptyConfig ? undefined : validateRequiredValue(value),
},
})} })}
/> />
{errors.v6?.range_end && ( {errors.v6?.range_end && (
@@ -133,14 +115,12 @@ const FormDHCPv6 = ({
{...register('v6.lease_duration', { {...register('v6.lease_duration', {
valueAsNumber: true, valueAsNumber: true,
validate: { validate: {
required: (value) => isEmptyConfig ? undefined : validateRequiredValue(value), required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
} },
})} })}
/> />
{errors.v6?.lease_duration && ( {errors.v6?.lease_duration && (
<div className="form__message form__message--error"> <div className="form__message form__message--error">{t(errors.v6.lease_duration.message)}</div>
{t(errors.v6.lease_duration.message)}
</div>
)} )}
</div> </div>
</div> </div>
@@ -149,8 +129,13 @@ const FormDHCPv6 = ({
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard" className="btn btn-success btn-standard"
disabled={isSubmitting || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig || Object.keys(errors).length > 0} disabled={
> isSubmitting ||
processingConfig ||
!isInterfaceIncludesIpv6 ||
isEmptyConfig ||
Object.keys(errors).length > 0
}>
{t('save_config')} {t('save_config')}
</button> </button>
</div> </div>

View File

@@ -1,20 +1,11 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useForm } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { validateRequiredValue } from '../../../helpers/validators'; import { validateRequiredValue } from '../../../helpers/validators';
import { RootState } from '../../../initialState'; import { RootState } from '../../../initialState';
import { DhcpFormValues } from '.';
type FormValues = {
interface_name: string;
};
type InterfacesProps = {
initialValues?: {
interface_name: string;
};
};
const renderInterfaces = (interfaces: any) => const renderInterfaces = (interfaces: any) =>
Object.keys(interfaces).map((item) => { Object.keys(interfaces).map((item) => {
@@ -82,19 +73,15 @@ const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: R
</div> </div>
); );
const Interfaces = ({ initialValues }: InterfacesProps) => { const Interfaces = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp);
const { const {
register, register,
watch, watch,
formState: { errors }, formState: { errors },
} = useForm<FormValues>({ } = useFormContext<DhcpFormValues>();
mode: 'onChange',
defaultValues: initialValues, const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp);
});
const interface_name = watch('interface_name'); const interface_name = watch('interface_name');
@@ -116,24 +103,22 @@ const Interfaces = ({ initialValues }: InterfacesProps) => {
disabled={enabled} disabled={enabled}
{...register('interface_name', { {...register('interface_name', {
validate: validateRequiredValue, validate: validateRequiredValue,
})} })}>
>
<option value="" disabled={enabled}> <option value="" disabled={enabled}>
{t('dhcp_interface_select')} {t('dhcp_interface_select')}
</option> </option>
{renderInterfaces(interfaces)} {renderInterfaces(interfaces)}
</select> </select>
{errors.interface_name && ( {errors.interface_name && (
<div className="form__message form__message--error"> <div className="form__message form__message--error">{t(errors.interface_name.message)}</div>
{t(errors.interface_name.message)}
</div>
)} )}
</div> </div>
{interfaceValue && renderInterfaceValues({ {interfaceValue &&
gateway_ip: interfaceValue.gateway_ip, renderInterfaceValues({
hardware_address: interfaceValue.hardware_address, gateway_ip: interfaceValue.gateway_ip,
ip_addresses: interfaceValue.ip_addresses hardware_address: interfaceValue.hardware_address,
})} ip_addresses: interfaceValue.ip_addresses,
})}
</div> </div>
); );
}; };

View File

@@ -4,8 +4,8 @@ import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { destroy } from 'redux-form'; import { FormProvider, useForm } from 'react-hook-form';
import { DHCP_DESCRIPTION_PLACEHOLDERS, DHCP_FORM_NAMES, STATUS_RESPONSE, FORM_NAME } from '../../../helpers/constants'; import { DHCP_DESCRIPTION_PLACEHOLDERS, STATUS_RESPONSE } from '../../../helpers/constants';
import Leases from './Leases'; import Leases from './Leases';
@@ -40,6 +40,36 @@ import {
import './index.css'; import './index.css';
import { RootState } from '../../../initialState'; import { RootState } from '../../../initialState';
export type DhcpFormValues = {
v4?: {
gateway_ip?: string;
subnet_mask?: string;
range_start?: string;
range_end?: string;
lease_duration?: number;
};
v6?: {
range_start?: string;
range_end?: string;
lease_duration?: number;
};
interface_name?: string;
};
const DEFAULT_V4_VALUES = {
gateway_ip: '',
subnet_mask: '',
range_start: '',
range_end: '',
lease_duration: 0,
};
const DEFAULT_V6_VALUES = {
range_start: '',
range_end: '',
lease_duration: 0,
};
const Dhcp = () => { const Dhcp = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -65,14 +95,21 @@ const Dhcp = () => {
modalType, modalType,
} = useSelector((state: RootState) => state.dhcp, shallowEqual); } = useSelector((state: RootState) => state.dhcp, shallowEqual);
const interface_name = useSelector( const methods = useForm<DhcpFormValues>({
(state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name, mode: 'onChange',
); defaultValues: {
v4: v4 || DEFAULT_V4_VALUES,
v6: v6 || DEFAULT_V6_VALUES,
interface_name: interfaceName || '',
},
});
const { watch, reset } = methods;
const interface_name = watch('interface_name');
const isInterfaceIncludesIpv4 = useSelector( const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses, (state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
); );
const ipv4Config = watch('v4');
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4); const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6); const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
@@ -87,6 +124,16 @@ const Dhcp = () => {
} }
}, [dhcp_available]); }, [dhcp_available]);
useEffect(() => {
if (v4 || v6 || interfaceName) {
reset({
v4: v4 || DEFAULT_V4_VALUES,
v6: v6 || DEFAULT_V6_VALUES,
interface_name: interfaceName || '',
});
}
}, [v4, v6, interfaceName, reset]);
useEffect(() => { useEffect(() => {
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? []; const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? []; const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
@@ -105,7 +152,11 @@ const Dhcp = () => {
const clear = () => { const clear = () => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm(t('dhcp_reset'))) { 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(resetDhcp());
dispatch(getDhcpStatus()); dispatch(getDhcpStatus());
} }
@@ -170,9 +221,6 @@ const Dhcp = () => {
const toggleModal = () => dispatch(toggleLeaseModal()); const toggleModal = () => dispatch(toggleLeaseModal());
const initialV4 = enteredSomeV4Value ? v4 : {};
const initialV6 = enteredSomeV6Value ? v6 : {};
if (processing || processingInterfaces) { if (processing || processingInterfaces) {
return <Loading />; return <Loading />;
} }
@@ -193,15 +241,13 @@ const Dhcp = () => {
const toggleDhcpButton = getToggleDhcpButton(); 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( const disabledLeasesButton = Boolean(
dhcp?.syncErrors || !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values, !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,
); );
const cidr = inputtedIPv4values const cidr = inputtedIPv4values ? `${ipv4Config.gateway_ip}/${subnetMaskToBitMask(ipv4Config.subnet_mask)}` : '';
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
: '';
return ( return (
<> <>
@@ -239,29 +285,32 @@ const Dhcp = () => {
</div> </div>
)} )}
<Interfaces initialValues={{ interface_name: interfaceName }} /> <FormProvider {...methods}>
<Interfaces />
<Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings"> <Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings">
<div> <div>
<FormDHCPv4 <FormDHCPv4
onSubmit={handleSubmit} onSubmit={handleSubmit}
initialValues={{ v4: initialV4 }} processingConfig={processingConfig}
processingConfig={processingConfig} ipv4placeholders={ipv4placeholders}
ipv4placeholders={ipv4placeholders} interfaces={interfaces}
/> />
</div> </div>
</Card> </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_ipv6_settings')} bodyType="card-body box-body--settings">
<div>
<FormDHCPv6
onSubmit={handleSubmit}
initialValues={{ v6: initialV6 }}
processingConfig={processingConfig}
ipv6placeholders={ipv6placeholders}
/>
</div>
</Card>
{enabled && ( {enabled && (
<Card title={t('dhcp_leases')} bodyType="card-body box-body--settings"> <Card title={t('dhcp_leases')} bodyType="card-body box-body--settings">
<div className="row"> <div className="row">
@@ -283,7 +332,7 @@ const Dhcp = () => {
processingDeleting={processingDeleting} processingDeleting={processingDeleting}
processingUpdating={processingUpdating} processingUpdating={processingUpdating}
cidr={cidr} cidr={cidr}
gatewayIp={dhcp?.values?.v4?.gateway_ip} gatewayIp={ipv4Config.gateway_ip}
/> />
<div className="btn-list mt-2"> <div className="btn-list mt-2">

View File

@@ -181,7 +181,6 @@ export const Form = ({
if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) { if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) {
// TODO onChange TLS config validation // TODO onChange TLS config validation
console.log('TLS config validation');
previousValuesRef.current = watchedValues; previousValuesRef.current = watchedValues;
} }
}, [watchedValues]); }, [watchedValues]);

View File

@@ -1,63 +0,0 @@
import React from 'react';
type Props = {
id?: string;
className?: string;
placeholder?: string;
type?: string;
disabled?: boolean;
autoComplete?: string;
isActionAvailable?: boolean;
removeField?: () => void;
normalizeOnBlur?: (value: string) => string;
value: string;
onChange: (value: string) => void;
onBlur: () => void;
};
export const InputGroup = ({
id,
className,
placeholder,
type,
disabled,
autoComplete,
isActionAvailable,
removeField,
normalizeOnBlur,
value,
onChange,
onBlur,
}: Props) => {
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
if (normalizeOnBlur) {
onChange(normalizeOnBlur(event.target.value));
}
onBlur();
};
return (
<div className="input-group">
<input
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={handleBlur}
/>
{isActionAvailable && (
<span className="input-group-append">
<button type="button" className="btn btn-secondary btn-icon btn-icon--green" onClick={removeField}>
<svg className="icon icon--24">
<use xlinkHref="#cross" />
</svg>
</button>
</span>
)}
</div>
);
};

View File

@@ -7,26 +7,23 @@ interface Props extends ComponentProps<'textarea'> {
error?: string; error?: string;
} }
export const Textarea = forwardRef<HTMLTextAreaElement, Props>( export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ name, label, className, error, ...rest }, ref) => (
({ name, label, className, error, onClick, ...rest }, ref) => ( <div className={clsx('form-group', { 'has-error': !!error })}>
<div className={clsx('form-group', { 'has-error': !!error })}> {label && (
{label && ( <label className="form__label" htmlFor={name}>
<label className="form__label" htmlFor={name}> {label}
{label} </label>
</label> )}
<textarea
className={clsx(
'form-control form-control--textarea form-control--textarea-small font-monospace',
className,
)} )}
<textarea ref={ref}
onClick={onClick} {...rest}
className={clsx( />
'form-control form-control--textarea form-control--textarea-small font-monospace', {error && <div className="form__message form__message--error">{error}</div>}
className, </div>
)} ));
ref={ref}
{...rest}
/>
{error && <div className="form__message form__message--error">{error}</div>}
</div>
),
);
Textarea.displayName = 'Textarea'; Textarea.displayName = 'Textarea';

View File

@@ -757,12 +757,9 @@ type NestedObject = {
order: number; order: number;
}; };
export const getObjectKeysSorted = < export const getObjectKeysSorted = <T extends Record<string, NestedObject>, K extends keyof NestedObject>(
T extends Record<string, NestedObject>,
K extends keyof NestedObject
>(
object: T, object: T,
sortKey: K sortKey: K,
): string[] => { ): string[] => {
return Object.entries(object) return Object.entries(object)
.sort(([, a], [, b]) => (a[sortKey] as number) - (b[sortKey] as number)) .sort(([, a], [, b]) => (a[sortKey] as number) - (b[sortKey] as number))