diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index f1178b81..0e82a444 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -206,6 +206,8 @@ "anonymize_client_ip": "Anonymize client IP", "anonymize_client_ip_desc": "Don't save the full IP address of the client in logs and statistics", "dns_config": "DNS server configuration", + "dns_cache_config": "DNS cache configuration", + "dns_cache_config_desc": "Here you can configure DNS cache", "blocking_mode": "Blocking mode", "default": "Default", "nxdomain": "NXDOMAIN", @@ -491,5 +493,16 @@ "list_updated": "{{count}} list updated", "list_updated_plural": "{{count}} lists updated", "dnssec_enable": "Enable DNSSEC", - "dnssec_enable_desc": "Set DNSSEC flag in the outcoming DNS queries and check the result (DNSSEC-enabled resolver is required)" + "dnssec_enable_desc": "Set DNSSEC flag in the outcoming DNS queries and check the result (DNSSEC-enabled resolver is required)", + "cache_size": "Cache size", + "cache_size_desc": "DNS cache size (in bytes)", + "cache_ttl_min_override": "Override minimum TTL", + "cache_ttl_max_override": "Override maximum TTL", + "enter_cache_size": "Enter cache size", + "enter_cache_ttl_min_override": "Enter minimum TTL", + "enter_cache_ttl_max_override": "Enter maximum TTL", + "cache_ttl_min_override_desc": "Override TTL value (minimum) received from upstream server. This value can't larger than 3600 (1 hour)", + "cache_ttl_max_override_desc": "Override TTL value (maximum) received from upstream server", + "min_exceeds_max_value": "Minimum value exceeds maximum value", + "value_not_larger_than": "Value can't be larger than {{maximum}}" } diff --git a/client/src/components/Settings/Dns/Cache/Form.js b/client/src/components/Settings/Dns/Cache/Form.js new file mode 100644 index 00000000..c1314e89 --- /dev/null +++ b/client/src/components/Settings/Dns/Cache/Form.js @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { Trans, useTranslation } from 'react-i18next'; +import { shallowEqual, useSelector } from 'react-redux'; +import { + biggerOrEqualZero, + maxValue, + renderInputField, + required, + toNumber, +} from '../../../../helpers/form'; +import { FORM_NAME } from '../../../../helpers/constants'; + +const maxValue3600 = maxValue(3600); + +const getInputFields = ({ required, maxValue3600 }) => [{ + name: 'cache_size', + title: 'cache_size', + description: 'cache_size_desc', + placeholder: 'enter_cache_size', + validate: required, +}, +{ + name: 'cache_ttl_min', + title: 'cache_ttl_min_override', + description: 'cache_ttl_min_override_desc', + placeholder: 'enter_cache_ttl_min_override', + max: 3600, + validate: maxValue3600, +}, +{ + name: 'cache_ttl_max', + title: 'cache_ttl_max_override', + description: 'cache_ttl_max_override_desc', + placeholder: 'enter_cache_ttl_max_override', +}]; + +const Form = ({ + handleSubmit, submitting, invalid, +}) => { + const { t } = useTranslation(); + + const { processingSetConfig } = useSelector((state) => state.dnsConfig, shallowEqual); + const { + cache_ttl_max, cache_ttl_min, + } = useSelector((state) => state.form[FORM_NAME.CACHE].values, shallowEqual); + + const minExceedsMax = cache_ttl_min > cache_ttl_max; + + const INPUTS_FIELDS = getInputFields({ + required, + maxValue3600, + }); + + return
+
+ {INPUTS_FIELDS.map(({ + name, title, description, placeholder, validate, max, + }) =>
+
+
+ +
{t(description)}
+ +
+
+
)} + {minExceedsMax + && {t('min_exceeds_max_value')}} +
+ +
; +}; + +Form.propTypes = { + handleSubmit: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + invalid: PropTypes.bool.isRequired, +}; + +export default reduxForm({ form: FORM_NAME.CACHE })(Form); diff --git a/client/src/components/Settings/Dns/Cache/index.js b/client/src/components/Settings/Dns/Cache/index.js new file mode 100644 index 00000000..22b8b7b6 --- /dev/null +++ b/client/src/components/Settings/Dns/Cache/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import Card from '../../../ui/Card'; +import Form from './Form'; +import { setDnsConfig } from '../../../../actions/dnsConfig'; +import { selectCompletedFields } from '../../../../helpers/helpers'; + +const CacheConfig = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { + cache_size, cache_ttl_max, cache_ttl_min, + } = useSelector((state) => state.dnsConfig, shallowEqual); + + const handleFormSubmit = (values) => { + const completedFields = selectCompletedFields(values); + dispatch(setDnsConfig(completedFields)); + }; + + return ( + +
+
+
+
+ ); +}; + +export default CacheConfig; diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js index 5ed8f9ca..41993f89 100644 --- a/client/src/components/Settings/Dns/index.js +++ b/client/src/components/Settings/Dns/index.js @@ -7,9 +7,10 @@ import Access from './Access'; import Config from './Config'; import PageTitle from '../../ui/PageTitle'; import Loading from '../../ui/Loading'; +import CacheConfig from './Cache'; const Dns = (props) => { - const [t] = useTranslation(); + const { t } = useTranslation(); useEffect(() => { props.getAccessList(); @@ -40,6 +41,10 @@ const Dns = (props) => { dnsConfig={dnsConfig} setDnsConfig={setDnsConfig} /> + } diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 60c22264..9d587d9e 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -406,4 +406,5 @@ export const FORM_NAME = { STATS_CONFIG: 'statsConfig', INSTALL: 'install', LOGIN: 'login', + CACHE: 'cache', }; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index bace77b5..f28ec5f3 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -1,6 +1,7 @@ import React, { Fragment } from 'react'; import { Trans } from 'react-i18next'; import PropTypes from 'prop-types'; +import i18next from 'i18next'; import { R_IPV4, R_MAC, R_HOST, R_IPV6, R_CIDR, R_CIDR_IPV6, UNSAFE_PORTS, R_URL_REQUIRES_PROTOCOL, R_WIN_ABSOLUTE_PATH, R_UNIX_ABSOLUTE_PATH, @@ -10,7 +11,7 @@ import { createOnBlurHandler } from './helpers'; export const renderField = (props, elementType) => { const { input, id, className, placeholder, type, disabled, normalizeOnBlur, - autoComplete, meta: { touched, error }, + autoComplete, meta: { touched, error }, min, max, step, } = props; const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur); @@ -23,14 +24,17 @@ export const renderField = (props, elementType) => { autoComplete, disabled, type, + min, + max, + step, onBlur, }); return ( - + <> {element} {!disabled && touched && error && {error}} - + ); }; @@ -43,6 +47,9 @@ renderField.propTypes = { disabled: PropTypes.bool, autoComplete: PropTypes.bool, normalizeOnBlur: PropTypes.func, + min: PropTypes.number, + max: PropTypes.number, + step: PropTypes.number, meta: PropTypes.shape({ touched: PropTypes.bool, error: PropTypes.object, @@ -238,6 +245,8 @@ export const required = (value) => { return form_error_required; }; +export const maxValue = (maximum) => (value) => (value && value > maximum ? i18next.t('value_not_larger_than', { maximum }) : undefined); + export const ipv4 = (value) => { if (value && !R_IPV4.test(value)) { return form_error_ip4_format; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index be8e884e..3a655ac3 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -562,3 +562,15 @@ export const getIpMatchListStatus = (ip, list) => { return IP_MATCH_LIST_STATUS.NOT_FOUND; } }; + +/** + * @param values {object} + * @returns {object} + */ +export const selectCompletedFields = (values) => Object.entries(values) + .reduce((acc, [key, value]) => { + if (value || value === 0) { + acc[key] = value; + } + return acc; + }, {});