Pull request #2231: ADG-8368 Frontend rewritten in TypeScript, added Node 18 support

Merge in DNS/adguard-home from ADG-8368-typescript-node-18 to master

Squashed commit of the following:

commit daa288ae0d76178af24595cc807055902e6f09ab
Merge: 4c89cf720 1085d59a6
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Jun 10 17:22:20 2024 +0200

    merge

commit 4c89cf7209
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Jun 6 13:27:18 2024 +0300

    remove install from initial state

commit b943f2011f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 23:10:55 2024 +0200

    frontend production build fix

commit cd1be2d66d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 20:23:14 2024 +0200

    production build quickfix

commit 7b8ac01fc2
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 5 19:57:31 2024 +0300

    all: upd node docker

commit 02afed66d5
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:23:12 2024 +0200

    changelog fixes

commit 9c0f736f0c
Merge: 62c4fbf1e e04775c4f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:18:29 2024 +0200

    merge

commit 62c4fbf1e3
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:22:22 2024 +0200

    empty line in changelog

commit 76b1e44a93
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:20:37 2024 +0200

    changelog

commit f783e90040
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:19:13 2024 +0200

    filters.js -> filters.ts

commit 3d4ce6554c
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:18:03 2024 +0200

    generated file removed

commit e35ba58f2a
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:45:21 2024 +0200

    rollback unwanted changes

commit 1f30d4216d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:27:36 2024 +0200

    review fix

commit 6cd4e44f07
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:55:39 2024 +0200

    missing generated file restoresd

commit 2ab738b303
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:40:32 2024 +0200

    Frontend rewritten in TypeScript, added Node 18 support
This commit is contained in:
Igor Lobanov
2024-06-10 18:42:23 +03:00
parent 1085d59a65
commit 1afe226ce8
296 changed files with 32122 additions and 32651 deletions

View File

@@ -1,162 +0,0 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import {
renderInputField,
toNumber,
} from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateNotInRange,
} from '../../../helpers/validators';
const FormDHCPv4 = ({
handleSubmit,
submitting,
processingConfig,
ipv4placeholders,
}) => {
const { t } = useTranslation();
const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const interfaces = useSelector((state) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv4 = useSelector(
(state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
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]);
return <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="v4.gateway_ip"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.gateway_ip)}
validate={[
validateIpv4,
validateRequired,
validateNotInRange,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="v4.subnet_mask"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.subnet_mask)}
validate={[
validateRequired,
validateGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v4.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_start)}
validate={[
validateIpv4,
validateIpForGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="col">
<Field
name="v4.range_end"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_end)}
validate={[
validateIpv4,
validateIpv4RangeEnd,
validateIpForGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
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}
/>
</div>
</div>
</div>
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={invalid}
>
{t('save_config')}
</button>
</div>
</form>;
};
FormDHCPv4.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
change: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
ipv4placeholders: PropTypes.object.isRequired,
};
export default reduxForm({
form: FORM_NAME.DHCPv4,
})(FormDHCPv4);

View File

@@ -0,0 +1,166 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import { renderInputField, toNumber } from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateNotInRange,
} from '../../../helpers/validators';
import { RootState } from '../../../initialState';
interface FormDHCPv4Props {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
initialValues: { v4?: any };
processingConfig?: boolean;
change: (field: string, value: any) => void;
reset: () => void;
ipv4placeholders?: {
gateway_ip: string;
subnet_mask: string;
range_start: string;
range_end: string;
lease_duration: string;
};
}
const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholders }: FormDHCPv4Props) => {
const { t } = useTranslation();
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
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],
);
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="v4.gateway_ip"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.gateway_ip)}
validate={[validateIpv4, validateRequired, validateNotInRange]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="v4.subnet_mask"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.subnet_mask)}
validate={[validateRequired, validateGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v4.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_start)}
validate={[validateIpv4, validateIpForGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
<div className="col">
<Field
name="v4.range_end"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_end)}
validate={[validateIpv4, validateIpv4RangeEnd, validateIpForGatewaySubnetMask]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
</div>
</div>
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
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}
/>
</div>
</div>
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
{t('save_config')}
</button>
</div>
</form>
);
};
export default reduxForm<
Record<string, any>,
Omit<FormDHCPv4Props, 'submitting' | 'handleSubmit' | 'reset' | 'change'>
>({
form: FORM_NAME.DHCPv4,
})(FormDHCPv4);

View File

@@ -1,117 +0,0 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import {
renderInputField,
toNumber,
} from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
const FormDHCPv6 = ({
handleSubmit,
submitting,
processingConfig,
ipv6placeholders,
}) => {
const { t } = useTranslation();
const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv6], shallowEqual);
const interfaces = useSelector((state) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
const isInterfaceIncludesIpv6 = useSelector(
(state) => !!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]);
return <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v6.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv6placeholders.range_start)}
validate={[validateIpv6, validateRequired]}
disabled={!isInterfaceIncludesIpv6}
/>
</div>
<div className="col">
<Field
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
/>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6 form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
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}
/>
</div>
</div>
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={invalid}
>
{t('save_config')}
</button>
</div>
</form>;
};
FormDHCPv6.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
change: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
ipv6placeholders: PropTypes.object.isRequired,
};
export default reduxForm({
form: FORM_NAME.DHCPv6,
})(FormDHCPv6);

View File

@@ -0,0 +1,131 @@
import React, { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next';
import { renderInputField, toNumber } from '../../../helpers/form';
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
import { RootState } from '../../../initialState';
interface FormDHCPv6Props {
handleSubmit: (...args: unknown[]) => string;
submitting: boolean;
initialValues: {
v6?: any;
};
change: (field: string, value: any) => void;
reset: () => void;
processingConfig?: boolean;
ipv6placeholders?: {
range_start: string;
range_end: string;
lease_duration: string;
};
}
const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholders }: FormDHCPv6Props) => {
const { t } = useTranslation();
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv6], shallowEqual);
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
const interface_name = interfaces?.values?.interface_name;
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],
);
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="v6.range_start"
component={renderInputField}
type="text"
className="form-control"
placeholder={t(ipv6placeholders.range_start)}
validate={[validateIpv6, validateRequired]}
disabled={!isInterfaceIncludesIpv6}
/>
</div>
<div className="col">
<Field
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
/>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6 form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
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}
/>
</div>
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
{t('save_config')}
</button>
</div>
</form>
);
};
export default reduxForm<
Record<string, any>,
Omit<FormDHCPv6Props, 'handleSubmit' | 'change' | 'submitting' | 'reset'>
>({
form: FORM_NAME.DHCPv6,
})(FormDHCPv6);

View File

@@ -1,108 +0,0 @@
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import propTypes from 'prop-types';
import { renderSelectField } from '../../../helpers/form';
import { validateRequiredValue } from '../../../helpers/validators';
import { FORM_NAME } from '../../../helpers/constants';
const renderInterfaces = (interfaces) => Object.keys(interfaces)
.map((item) => {
const option = interfaces[item];
const { name } = option;
const [interfaceIPv4] = option?.ipv4_addresses ?? [];
const [interfaceIPv6] = option?.ipv6_addresses ?? [];
const optionContent = [name, interfaceIPv4, interfaceIPv6].filter(Boolean).join(' - ');
return <option value={name} key={name}>{optionContent}</option>;
});
const getInterfaceValues = ({
gateway_ip,
hardware_address,
ip_addresses,
}) => [
{
name: 'dhcp_form_gateway_input',
value: gateway_ip,
},
{
name: 'dhcp_hardware_address',
value: hardware_address,
},
{
name: 'dhcp_ip_addresses',
value: ip_addresses,
render: (ip_addresses) => ip_addresses
.map((ip) => <span key={ip} className="interface__ip">{ip}</span>),
},
];
const renderInterfaceValues = ({
gateway_ip,
hardware_address,
ip_addresses,
}) => <div className='d-flex align-items-end dhcp__interfaces-info'>
<ul className="list-unstyled m-0">
{getInterfaceValues({
gateway_ip,
hardware_address,
ip_addresses,
}).map(({ name, value, render }) => value && <li key={name}>
<span className="interface__title"><Trans>{name}</Trans>: </span>
{render?.(value) || value}
</li>)}
</ul>
</div>;
const Interfaces = () => {
const { t } = useTranslation();
const {
processingInterfaces,
interfaces,
enabled,
} = useSelector((store) => store.dhcp, shallowEqual);
const interface_name = useSelector(
(store) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name,
);
if (processingInterfaces || !interfaces) {
return null;
}
const interfaceValue = interface_name && interfaces[interface_name];
return <div className="row dhcp__interfaces">
<div className="col col__dhcp">
<Field
name="interface_name"
component={renderSelectField}
className="form-control custom-select pl-4 col-md"
validate={[validateRequiredValue]}
label='dhcp_interface_select'
>
<option value='' disabled={enabled}>
{t('dhcp_interface_select')}
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
{interfaceValue
&& renderInterfaceValues(interfaceValue)}
</div>;
};
renderInterfaceValues.propTypes = {
gateway_ip: propTypes.string.isRequired,
hardware_address: propTypes.string.isRequired,
ip_addresses: propTypes.arrayOf(propTypes.string).isRequired,
};
export default reduxForm({
form: FORM_NAME.DHCP_INTERFACES,
})(Interfaces);

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-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';
const renderInterfaces = (interfaces: any) =>
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
const [interfaceIPv4] = option?.ipv4_addresses ?? [];
const [interfaceIPv6] = option?.ipv6_addresses ?? [];
const optionContent = [name, interfaceIPv4, interfaceIPv6].filter(Boolean).join(' - ');
return (
<option value={name} key={name}>
{optionContent}
</option>
);
});
const getInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: any) => [
{
name: 'dhcp_form_gateway_input',
value: gateway_ip,
},
{
name: 'dhcp_hardware_address',
value: hardware_address,
},
{
name: 'dhcp_ip_addresses',
value: ip_addresses,
render: (ip_addresses: any) =>
ip_addresses.map((ip: any) => (
<span key={ip} className="interface__ip">
{ip}
</span>
)),
},
];
interface renderInterfaceValuesProps {
gateway_ip: string;
hardware_address: string;
ip_addresses: string[];
}
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({
gateway_ip,
hardware_address,
ip_addresses,
}).map(
({ name, value, render }) =>
value && (
<li key={name}>
<span className="interface__title">
<Trans>{name}</Trans>:{' '}
</span>
{render?.(value) || value}
</li>
),
)}
</ul>
</div>
);
const Interfaces = () => {
const { t } = useTranslation();
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp, shallowEqual);
const interface_name =
useSelector((store: RootState) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
if (processingInterfaces || !interfaces) {
return null;
}
const interfaceValue = interface_name && interfaces[interface_name];
return (
<div className="row dhcp__interfaces">
<div className="col col__dhcp">
<Field
name="interface_name"
component={renderSelectField}
className="form-control custom-select pl-4 col-md"
validate={[validateRequiredValue]}
label="dhcp_interface_select">
<option value="" disabled={enabled}>
{t('dhcp_interface_select')}
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
{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);

View File

@@ -1,14 +1,24 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import { Trans, withTranslation } from 'react-i18next';
import { LEASES_TABLE_DEFAULT_PAGE_SIZE, MODAL_TYPE } from '../../../helpers/constants';
import { sortIp } from '../../../helpers/helpers';
import { toggleLeaseModal } from '../../../actions';
class Leases extends Component {
cellWrap = ({ value }) => (
interface LeasesProps {
leases?: unknown[];
t?: (...args: unknown[]) => string;
dispatch?: (...args: unknown[]) => unknown;
disabledLeasesButton?: boolean;
}
class Leases extends Component<LeasesProps> {
cellWrap = ({ value }: any) => (
<div className="logs__row o-hidden">
<span className="logs__text" title={value}>
{value}
@@ -16,15 +26,17 @@ class Leases extends Component {
</div>
);
convertToStatic = (data) => () => {
convertToStatic = (data: any) => () => {
const { dispatch } = this.props;
dispatch(toggleLeaseModal({
type: MODAL_TYPE.ADD_LEASE,
config: data,
}));
}
dispatch(
toggleLeaseModal({
type: MODAL_TYPE.ADD_LEASE,
config: data,
}),
);
};
makeStatic = ({ row }) => {
makeStatic = ({ row }: any) => {
const { t, disabledLeasesButton } = this.props;
return (
<div className="logs__row logs__row--center">
@@ -33,15 +45,14 @@ class Leases extends Component {
className="btn btn-icon btn-icon--green btn-outline-success btn-sm"
title={t('make_static')}
onClick={this.convertToStatic(row)}
disabled={disabledLeasesButton}
>
disabled={disabledLeasesButton}>
<svg className="icons icon12">
<use xlinkHref="#plus" />
</svg>
</button>
</div>
);
}
};
render() {
const { leases, t } = this.props;
@@ -54,23 +65,27 @@ class Leases extends Component {
accessor: 'mac',
minWidth: 180,
Cell: this.cellWrap,
}, {
},
{
Header: 'IP',
accessor: 'ip',
minWidth: 230,
Cell: this.cellWrap,
sortMethod: sortIp,
}, {
},
{
Header: <Trans>dhcp_table_hostname</Trans>,
accessor: 'hostname',
minWidth: 230,
Cell: this.cellWrap,
}, {
},
{
Header: <Trans>dhcp_table_expires</Trans>,
accessor: 'expires',
minWidth: 220,
Cell: this.cellWrap,
}, {
},
{
Header: <Trans>actions_table_header</Trans>,
Cell: this.makeStatic,
},
@@ -86,11 +101,9 @@ class Leases extends Component {
}
}
Leases.propTypes = {
leases: PropTypes.array,
t: PropTypes.func,
dispatch: PropTypes.func,
disabledLeasesButton: PropTypes.bool,
};
export default withTranslation()(connect(() => ({}), (dispatch) => ({ dispatch }))(Leases));
export default withTranslation()(
connect(
() => ({}),
(dispatch) => ({ dispatch }),
)(Leases),
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
@@ -13,20 +13,32 @@ import {
validateIpGateway,
} from '../../../../helpers/validators';
import { FORM_NAME } from '../../../../helpers/constants';
import { toggleLeaseModal } from '../../../../actions';
const Form = ({
handleSubmit,
reset,
pristine,
submitting,
processingAdding,
cidr,
isEdit,
}) => {
import { toggleLeaseModal } from '../../../../actions';
import { RootState } from '../../../../initialState';
interface FormStaticLeaseProps {
initialValues?: {
mac?: string;
ip?: string;
hostname?: string;
cidr?: string;
gatewayIp?: string;
};
pristine: boolean;
handleSubmit: (...args: unknown[]) => string;
reset: () => void;
submitting: boolean;
processingAdding?: boolean;
cidr?: string;
isEdit?: boolean;
}
const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cidr, isEdit }: FormStaticLeaseProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const dynamicLease = useSelector((store) => store.dhcp.leaseModalConfig, shallowEqual);
const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);
const onClick = () => {
reset();
@@ -49,6 +61,7 @@ const Form = ({
disabled={isEdit}
/>
</div>
<div className="form__group">
<Field
id="ip"
@@ -57,14 +70,10 @@ const Form = ({
type="text"
className="form-control"
placeholder={t('form_enter_subnet_ip', { cidr })}
validate={[
validateRequiredValue,
validateIpv4,
validateIpv4InCidr,
validateIpGateway,
]}
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr, validateIpGateway]}
/>
</div>
<div className="form__group">
<Field
id="hostname"
@@ -83,15 +92,14 @@ const Form = ({
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={onClick}
>
onClick={onClick}>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || processingAdding || (pristine && !dynamicLease)}
>
disabled={submitting || processingAdding || (pristine && !dynamicLease)}>
<Trans>save_btn</Trans>
</button>
</div>
@@ -100,21 +108,7 @@ const Form = ({
);
};
Form.propTypes = {
initialValues: PropTypes.shape({
mac: PropTypes.string.isRequired,
ip: PropTypes.string.isRequired,
hostname: PropTypes.string.isRequired,
cidr: PropTypes.string.isRequired,
gatewayIp: PropTypes.string,
}),
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
processingAdding: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
isEdit: PropTypes.bool,
};
export default reduxForm({ form: FORM_NAME.LEASE })(Form);
export default reduxForm<
Record<string, any>,
Omit<FormStaticLeaseProps, 'submitting' | 'handleSubmit' | 'reset' | 'pristine'>
>({ form: FORM_NAME.LEASE })(Form);

View File

@@ -1,11 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import { toggleLeaseModal } from '../../../../actions';
import { MODAL_TYPE } from '../../../../helpers/constants';
import { RootState } from '../../../../initialState';
interface ModalProps {
isModalOpen: boolean;
modalType: string;
handleSubmit: (values: any) => void;
processingAdding: boolean;
cidr: string;
gatewayIp?: string;
}
const Modal = ({
isModalOpen,
@@ -13,24 +25,20 @@ const Modal = ({
handleSubmit,
processingAdding,
cidr,
rangeStart,
rangeEnd,
gatewayIp,
}) => {
}: ModalProps) => {
const dispatch = useDispatch();
const toggleModal = () => dispatch(toggleLeaseModal());
const leaseInitialData = useSelector(
(state) => state.dhcp.leaseModalConfig, shallowEqual,
) || {};
const leaseInitialData = useSelector((state: RootState) => state.dhcp.leaseModalConfig, shallowEqual);
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={toggleModal}
>
onRequestClose={toggleModal}>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
@@ -40,25 +48,23 @@ const Modal = ({
<Trans>dhcp_new_static_lease</Trans>
)}
</h4>
<button type="button" className="close" onClick={toggleModal}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{
mac: leaseInitialData.mac ?? '',
ip: leaseInitialData.ip ?? '',
hostname: leaseInitialData.hostname ?? '',
mac: leaseInitialData?.mac ?? '',
ip: leaseInitialData?.ip ?? '',
hostname: leaseInitialData?.hostname ?? '',
cidr,
rangeStart,
rangeEnd,
gatewayIp,
}}
onSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
isEdit={modalType === MODAL_TYPE.EDIT_LEASE}
/>
</div>
@@ -66,15 +72,4 @@ const Modal = ({
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
handleSubmit: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
rangeStart: PropTypes.string,
rangeEnd: PropTypes.string,
gatewayIp: PropTypes.string,
};
export default withTranslation()(Modal);

View File

@@ -1,26 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { LEASES_TABLE_DEFAULT_PAGE_SIZE, MODAL_TYPE } from '../../../../helpers/constants';
import { sortIp } from '../../../../helpers/helpers';
import Modal from './Modal';
import {
addStaticLease,
removeStaticLease,
toggleLeaseModal,
updateStaticLease,
} from '../../../../actions';
const cellWrap = ({ value }) => (
import { sortIp } from '../../../../helpers/helpers';
import Modal from './Modal';
import { addStaticLease, removeStaticLease, toggleLeaseModal, updateStaticLease } from '../../../../actions';
interface cellWrapProps {
value: string;
}
const cellWrap = ({ value }: cellWrapProps) => (
<div className="logs__row o-hidden">
<span className="logs__text" title={value}>
{value}
</span>
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
interface StaticLeasesProps {
staticLeases: unknown[];
isModalOpen: boolean;
modalType: string;
processingAdding: boolean;
processingDeleting: boolean;
processingUpdating: boolean;
cidr: string;
gatewayIp?: string;
}
const StaticLeases = ({
isModalOpen,
modalType,
@@ -29,14 +42,12 @@ const StaticLeases = ({
processingUpdating,
staticLeases,
cidr,
rangeStart,
rangeEnd,
gatewayIp,
}) => {
}: StaticLeasesProps) => {
const [t] = useTranslation();
const dispatch = useDispatch();
const handleSubmit = (data) => {
const handleSubmit = (data: any) => {
const { mac, ip, hostname } = data;
if (modalType === MODAL_TYPE.EDIT_LEASE) {
@@ -46,15 +57,17 @@ const StaticLeases = ({
}
};
const handleDelete = (ip, mac, hostname = '') => {
const handleDelete = (ip: any, mac: any, hostname = '') => {
const name = hostname || ip;
// eslint-disable-next-line no-alert
if (window.confirm(t('delete_confirm', { key: name }))) {
dispatch(removeStaticLease({
ip,
mac,
hostname,
}));
dispatch(
removeStaticLease({
ip,
mac,
hostname,
}),
);
}
};
@@ -89,7 +102,7 @@ const StaticLeases = ({
sortable: false,
resizable: false,
// eslint-disable-next-line react/display-name
Cell: (row) => {
Cell: (row: any) => {
const { ip, mac, hostname } = row.original;
return (
@@ -97,24 +110,27 @@ const StaticLeases = ({
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() => dispatch(toggleLeaseModal({
type: MODAL_TYPE.EDIT_LEASE,
config: { ip, mac, hostname },
}))}
onClick={() =>
dispatch(
toggleLeaseModal({
type: MODAL_TYPE.EDIT_LEASE,
config: { ip, mac, hostname },
}),
)
}
disabled={processingUpdating}
title={t('edit_table_action')}
>
title={t('edit_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => handleDelete(ip, mac, hostname)}
disabled={processingDeleting}
title={t('delete_table_action')}
>
title={t('delete_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#delete" />
</svg>
@@ -131,35 +147,17 @@ const StaticLeases = ({
className="-striped -highlight card-table-overflow"
minRows={6}
/>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
handleSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
gatewayIp={gatewayIp}
/>
</>
);
};
StaticLeases.propTypes = {
staticLeases: PropTypes.array.isRequired,
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingDeleting: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
rangeStart: PropTypes.string,
rangeEnd: PropTypes.string,
gatewayIp: PropTypes.string,
};
cellWrap.propTypes = {
value: PropTypes.string.isRequired,
};
export default StaticLeases;

View File

@@ -1,312 +0,0 @@
import React, { useEffect, useState } from 'react';
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 Leases from './Leases';
import StaticLeases from './StaticLeases/index';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import {
findActiveDhcp,
getDhcpInterfaces,
getDhcpStatus,
resetDhcp,
setDhcpConfig,
resetDhcpLeases,
toggleDhcp,
toggleLeaseModal,
} from '../../../actions';
import FormDHCPv4 from './FormDHCPv4';
import FormDHCPv6 from './FormDHCPv6';
import Interfaces from './Interfaces';
import {
calculateDhcpPlaceholdersIpv4,
calculateDhcpPlaceholdersIpv6,
subnetMaskToBitMask,
} from '../../../helpers/helpers';
import './index.css';
const Dhcp = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
processingStatus,
processingConfig,
processing,
processingInterfaces,
check,
leases,
staticLeases,
isModalOpen,
processingAdding,
processingDeleting,
processingUpdating,
processingDhcp,
v4,
v6,
interface_name: interfaceName,
enabled,
dhcp_available,
interfaces,
modalType,
} = useSelector((state) => state.dhcp, shallowEqual);
const interface_name = useSelector(
(state) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name,
);
const isInterfaceIncludesIpv4 = useSelector(
(state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
useEffect(() => {
dispatch(getDhcpStatus());
}, []);
useEffect(() => {
if (dhcp_available) {
dispatch(getDhcpInterfaces());
}
}, [dhcp_available]);
useEffect(() => {
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
const gateway_ip = interfaces?.[interface_name]?.gateway_ip;
const v4placeholders = ipv4
? calculateDhcpPlaceholdersIpv4(ipv4, gateway_ip)
: DHCP_DESCRIPTION_PLACEHOLDERS.ipv4;
const v6placeholders = ipv6
? calculateDhcpPlaceholdersIpv6()
: DHCP_DESCRIPTION_PLACEHOLDERS.ipv6;
setIpv4Placeholders(v4placeholders);
setIpv6Placeholders(v6placeholders);
}, [interface_name]);
const clear = () => {
// eslint-disable-next-line no-alert
if (window.confirm(t('dhcp_reset'))) {
Object.values(DHCP_FORM_NAMES)
.forEach((formName) => dispatch(destroy(formName)));
dispatch(resetDhcp());
dispatch(getDhcpStatus());
}
};
const handleSubmit = (values) => {
dispatch(setDhcpConfig({
interface_name,
...values,
}));
};
const handleReset = () => {
if (window.confirm(t('dhcp_reset_leases_confirm'))) {
dispatch(resetDhcpLeases());
}
};
const enteredSomeV4Value = Object.values(v4)
.some(Boolean);
const enteredSomeV6Value = Object.values(v6)
.some(Boolean);
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
const getToggleDhcpButton = () => {
const filledConfig = interface_name && (Object.values(v4)
.every(Boolean) || Object.values(v6)
.every(Boolean));
const className = classNames('btn btn-sm', {
'btn-gray': enabled,
'btn-outline-success': !enabled,
});
const onClickDisable = () => dispatch(toggleDhcp({ enabled }));
const onClickEnable = () => {
const values = {
enabled,
interface_name,
v4: enteredSomeV4Value ? v4 : {},
v6: enteredSomeV6Value ? v6 : {},
};
dispatch(toggleDhcp(values));
};
return <button
type="button"
className={className}
onClick={enabled ? onClickDisable : onClickEnable}
disabled={processingDhcp || processingConfig
|| (!enabled && (!filledConfig || !check))}
>
<Trans>{enabled ? 'dhcp_disable' : 'dhcp_enable'}</Trans>
</button>;
};
const statusButtonClass = classNames('btn btn-sm dhcp-form__button', {
'btn-loading btn-primary': processingStatus,
'btn-outline-primary': !processingStatus,
});
const onClick = () => dispatch(findActiveDhcp(interface_name));
const toggleModal = () => dispatch(toggleLeaseModal());
const initialV4 = enteredSomeV4Value ? v4 : {};
const initialV6 = enteredSomeV6Value ? v6 : {};
if (processing || processingInterfaces) {
return <Loading />;
}
if (!processing && !dhcp_available) {
return <div className="text-center pt-5">
<h2>
<Trans>unavailable_dhcp</Trans>
</h2>
<h4>
<Trans>unavailable_dhcp_desc</Trans>
</h4>
</div>;
}
const toggleDhcpButton = getToggleDhcpButton();
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const disabledLeasesButton = Boolean(dhcp?.syncErrors || interfaces?.syncErrors
|| !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values);
const cidr = inputtedIPv4values ? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}` : '';
return <>
<PageTitle title={t('dhcp_settings')} subtitle={t('dhcp_description')} containerClass="page-title--dhcp">
{toggleDhcpButton}
<button
type="button"
className={statusButtonClass}
onClick={onClick}
disabled={enabled || !interface_name || processingConfig}
>
<Trans>check_dhcp_servers</Trans>
</button>
<button
type="button"
className='btn btn-sm btn-outline-secondary'
disabled={!enteredSomeValue || processingConfig}
onClick={clear}
>
<Trans>reset_settings</Trans>
</button>
</PageTitle>
{!processing && !processingInterfaces
&& <>
{!enabled
&& check
&& (check.v4.other_server.found !== STATUS_RESPONSE.NO
|| check.v6.other_server.found !== STATUS_RESPONSE.NO)
&& <div className="mb-5">
<hr />
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
</div>}
<Interfaces
initialValues={{ interface_name: interfaceName }}
/>
<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">
<div className="col">
<Leases leases={leases} disabledLeasesButton={disabledLeasesButton}/>
</div>
</div>
</Card>}
<Card
title={t('dhcp_static_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={staticLeases}
isModalOpen={isModalOpen}
toggleModal={toggleModal}
modalType={modalType}
processingAdding={processingAdding}
processingDeleting={processingDeleting}
processingUpdating={processingUpdating}
cidr={cidr}
rangeStart={dhcp?.values?.v4?.range_start}
rangeEnd={dhcp?.values?.v4?.range_end}
gatewayIp={dhcp?.values?.v4?.gateway_ip}
/>
<div className="btn-list mt-2">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={toggleModal}
disabled={disabledLeasesButton}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standard mt-3"
onClick={handleReset}
>
<Trans>dhcp_reset_leases</Trans>
</button>
</div>
</div>
</div>
</Card>
</>}
</>;
};
export default Dhcp;

View File

@@ -0,0 +1,321 @@
import React, { useEffect, useState } from 'react';
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 Leases from './Leases';
import StaticLeases from './StaticLeases/index';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import {
findActiveDhcp,
getDhcpInterfaces,
getDhcpStatus,
resetDhcp,
setDhcpConfig,
resetDhcpLeases,
toggleDhcp,
toggleLeaseModal,
} from '../../../actions';
import FormDHCPv4 from './FormDHCPv4';
import FormDHCPv6 from './FormDHCPv6';
import Interfaces from './Interfaces';
import {
calculateDhcpPlaceholdersIpv4,
calculateDhcpPlaceholdersIpv6,
subnetMaskToBitMask,
} from '../../../helpers/helpers';
import './index.css';
import { RootState } from '../../../initialState';
const Dhcp = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
processingStatus,
processingConfig,
processing,
processingInterfaces,
check,
leases,
staticLeases,
isModalOpen,
processingAdding,
processingDeleting,
processingUpdating,
processingDhcp,
v4,
v6,
interface_name: interfaceName,
enabled,
dhcp_available,
interfaces,
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 dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
useEffect(() => {
dispatch(getDhcpStatus());
}, []);
useEffect(() => {
if (dhcp_available) {
dispatch(getDhcpInterfaces());
}
}, [dhcp_available]);
useEffect(() => {
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
const gateway_ip = interfaces?.[interface_name]?.gateway_ip;
const v4placeholders = ipv4
? calculateDhcpPlaceholdersIpv4(ipv4, gateway_ip)
: DHCP_DESCRIPTION_PLACEHOLDERS.ipv4;
const v6placeholders = ipv6 ? calculateDhcpPlaceholdersIpv6() : DHCP_DESCRIPTION_PLACEHOLDERS.ipv6;
setIpv4Placeholders(v4placeholders);
setIpv6Placeholders(v6placeholders);
}, [interface_name]);
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)));
dispatch(resetDhcp());
dispatch(getDhcpStatus());
}
};
const handleSubmit = (values: any) => {
dispatch(
setDhcpConfig({
interface_name,
...values,
}),
);
};
const handleReset = () => {
if (window.confirm(t('dhcp_reset_leases_confirm'))) {
dispatch(resetDhcpLeases());
}
};
const enteredSomeV4Value = Object.values(v4).some(Boolean);
const enteredSomeV6Value = Object.values(v6).some(Boolean);
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
const getToggleDhcpButton = () => {
const filledConfig =
interface_name &&
(Object.values(v4)
.every(Boolean) ||
Object.values(v6).every(Boolean));
const className = classNames('btn btn-sm', {
'btn-gray': enabled,
'btn-outline-success': !enabled,
});
const onClickDisable = () => dispatch(toggleDhcp({ enabled }));
const onClickEnable = () => {
const values = {
enabled,
interface_name,
v4: enteredSomeV4Value ? v4 : {},
v6: enteredSomeV6Value ? v6 : {},
};
dispatch(toggleDhcp(values));
};
return (
<button
type="button"
className={className}
onClick={enabled ? onClickDisable : onClickEnable}
disabled={processingDhcp || processingConfig || (!enabled && (!filledConfig || !check))}>
<Trans>{enabled ? 'dhcp_disable' : 'dhcp_enable'}</Trans>
</button>
);
};
const statusButtonClass = classNames('btn btn-sm dhcp-form__button', {
'btn-loading btn-primary': processingStatus,
'btn-outline-primary': !processingStatus,
});
const onClick = () => dispatch(findActiveDhcp(interface_name));
const toggleModal = () => dispatch(toggleLeaseModal());
const initialV4 = enteredSomeV4Value ? v4 : {};
const initialV6 = enteredSomeV6Value ? v6 : {};
if (processing || processingInterfaces) {
return <Loading />;
}
if (!processing && !dhcp_available) {
return (
<div className="text-center pt-5">
<h2>
<Trans>unavailable_dhcp</Trans>
</h2>
<h4>
<Trans>unavailable_dhcp_desc</Trans>
</h4>
</div>
);
}
const toggleDhcpButton = getToggleDhcpButton();
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const disabledLeasesButton = Boolean(
dhcp?.syncErrors ||
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
processingConfig ||
!inputtedIPv4values,
);
const cidr = inputtedIPv4values
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
: '';
return (
<>
<PageTitle title={t('dhcp_settings')} subtitle={t('dhcp_description')} containerClass="page-title--dhcp">
{toggleDhcpButton}
<button
type="button"
className={statusButtonClass}
onClick={onClick}
disabled={enabled || !interface_name || processingConfig}>
<Trans>check_dhcp_servers</Trans>
</button>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
disabled={!enteredSomeValue || processingConfig}
onClick={clear}>
<Trans>reset_settings</Trans>
</button>
</PageTitle>
{!processing && !processingInterfaces && (
<>
{!enabled &&
check &&
(check.v4.other_server.found !== STATUS_RESPONSE.NO ||
check.v6.other_server.found !== STATUS_RESPONSE.NO) && (
<div className="mb-5">
<hr />
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
</div>
)}
<Interfaces initialValues={{ interface_name: interfaceName }} />
<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">
<div className="col">
<Leases leases={leases} disabledLeasesButton={disabledLeasesButton} />
</div>
</div>
</Card>
)}
<Card title={t('dhcp_static_leases')} bodyType="card-body box-body--settings">
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={staticLeases}
isModalOpen={isModalOpen}
modalType={modalType}
processingAdding={processingAdding}
processingDeleting={processingDeleting}
processingUpdating={processingUpdating}
cidr={cidr}
gatewayIp={dhcp?.values?.v4?.gateway_ip}
/>
<div className="btn-list mt-2">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={toggleModal}
disabled={disabledLeasesButton}>
<Trans>dhcp_add_static_lease</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standard mt-3"
onClick={handleReset}>
<Trans>dhcp_reset_leases</Trans>
</button>
</div>
</div>
</div>
</Card>
</>
)}
</>
);
};
export default Dhcp;