ADG-10295 dns settings form

This commit is contained in:
Ildar Kamalov
2025-08-27 18:58:25 +03:00
parent e841e3a0b4
commit 2d513044aa
48 changed files with 1965 additions and 112 deletions

View File

@@ -1,12 +1,5 @@
{
"client_settings": "Client settings",
"example_upstream_reserved": "an upstream <0>for specific domains</0>;",
"example_multiple_upstreams_reserved": "multiple upstreams <0>for specific domains</0>;",
"example_upstream_comment": "a comment.",
"upstream_parallel": "Use parallel queries to speed up resolving by querying all upstream servers simultaneously.",
"parallel_requests": "Parallel requests",
"load_balancing": "Load-balancing",
"load_balancing_desc": "Query one upstream server at a time.<br/>AdGuard Home uses a weighted random algorithm to select servers with the lowest number of failed lookups and the lowest average lookup time.",
"bootstrap_dns": "Bootstrap DNS servers",
"bootstrap_dns_desc": "IP addresses of DNS servers used to resolve IP addresses of the DoH/DoT resolvers you specify as upstreams. Comments are not permitted.",
"fallback_dns_title": "Fallback DNS servers",
@@ -17,10 +10,6 @@
"local_ptr_default_resolver": "By default, AdGuard Home uses the following reverse DNS resolvers: {{ip}}.",
"local_ptr_no_default_resolver": "AdGuard Home could not determine suitable private reverse DNS resolvers for this system.",
"local_ptr_placeholder": "Enter one IP address per line",
"resolve_clients_title": "Enable reverse resolving of clients' IP addresses",
"resolve_clients_desc": "Reversely resolve clients' IP addresses into their hostnames by sending PTR queries to corresponding resolvers (private DNS servers for local clients, upstream servers for clients with public IP addresses).",
"use_private_ptr_resolvers_title": "Use private reverse DNS resolvers",
"use_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, etc. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN.",
"check_dhcp_servers": "Check for DHCP servers",
"save_config": "Save configuration",
"enabled_dhcp": "DHCP server enabled",
@@ -155,7 +144,6 @@
"enforce_save_search_hint": "AdGuard Home will enforce safe search in the following search engines: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "No servers specified",
"general_settings": "General settings",
"dns_settings": "DNS settings",
"dns_blocklists": "DNS blocklists",
"dns_allowlists": "DNS allowlists",
"dns_blocklists_desc": "AdGuard Home will block domains matching the blocklists.",
@@ -163,13 +151,10 @@
"custom_filtering_rules": "Custom filtering rules",
"encryption_settings": "Encryption settings",
"dhcp_settings": "DHCP settings",
"upstream_dns": "Upstream DNS servers",
"upstream_dns_help": "Enter one server address per line. <a>Learn more</a> about configuring upstream DNS servers.",
"upstream_dns_configured_in_file": "Configured in {{path}}",
"test_upstream_btn": "Test upstreams",
"upstreams": "Upstreams",
"upstream": "Upstream",
"apply_btn": "Apply",
"disabled_filtering_toast": "Disabled filtering",
"enabled_filtering_toast": "Enabled filtering",
"disabled_safe_browsing_toast": "Disabled Safe Browsing",
@@ -210,25 +195,9 @@
"custom_filter_rules": "Custom filtering rules",
"custom_filter_rules_hint": "Enter one rule on a line. You can use either adblock rules or hosts files syntax.",
"system_host_files": "System hosts files",
"examples_title": "Examples",
"example_meaning_filter_block": "block access to example.org and all its subdomains;",
"example_meaning_filter_whitelist": "unblock access to example.org and all its subdomains;",
"example_meaning_host_block": "respond with 127.0.0.1 for example.org (but not for its subdomains);",
"example_comment": "! Here goes a comment.",
"example_comment_meaning": "just a comment;",
"example_comment_hash": "# Also a comment.",
"example_regex_meaning": "block access to domains matching the specified regular expression.",
"example_upstream_regular": "regular DNS (over UDP);",
"example_upstream_regular_port": "regular DNS (over UDP, with port);",
"example_upstream_udp": "regular DNS (over UDP, hostname);",
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>;",
"example_upstream_doh3": "encrypted DNS-over-HTTPS with forced <0>HTTP/3</0> and no fallback to HTTP/2 or below;",
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>;",
"example_upstream_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
"example_upstream_tcp": "regular DNS (over TCP);",
"example_upstream_tcp_port": "regular DNS (over TCP, with port);",
"example_upstream_tcp_hostname": "regular DNS (over TCP, hostname);",
"all_lists_up_to_date_toast": "All lists are already up-to-date",
"updated_upstream_dns_toast": "Upstream servers successfully saved",
"dns_test_ok_toast": "Specified DNS servers are working correctly",
@@ -281,18 +250,11 @@
"dns_cache_config": "DNS cache configuration",
"dns_cache_config_desc": "Here you can configure DNS cache",
"blocking_mode": "Blocking mode",
"default": "Default",
"nxdomain": "NXDOMAIN",
"refused": "REFUSED",
"null_ip": "Null IP",
"custom_ip": "Custom IP",
"blocking_ipv4": "Blocking IPv4",
"blocking_ipv6": "Blocking IPv6",
"blocked_response_ttl": "Blocked response TTL",
"blocked_response_ttl_desc": "Specifies for how many seconds the clients should cache a filtered response",
"form_enter_blocked_response_ttl": "Enter blocked response TTL (seconds)",
"upstream_timeout": "Upstream timeout",
"upstream_timeout_desc": "Specifies the number of seconds to wait for a response from the upstream server",
"form_enter_upstream_timeout": "Enter the upstream server timeout duration in seconds",
"dnscrypt": "DNSCrypt",
"dns_over_https": "DNS-over-HTTPS",
@@ -307,10 +269,6 @@
"plain_dns": "Plain DNS",
"form_enter_rate_limit": "Enter rate limit",
"rate_limit": "Rate limit",
"edns_enable": "Enable EDNS client subnet",
"edns_cs_desc": "Add the EDNS Client Subnet option (ECS) to upstream requests and log the values sent by the clients in the query log.",
"edns_use_custom_ip": "Use custom IP for EDNS",
"edns_use_custom_ip_desc": "Allow to use custom IP for EDNS",
"rate_limit_desc": "The number of requests per second allowed per client. Setting it to 0 means no limit.",
"rate_limit_subnet_len_ipv4": "Subnet prefix length for IPv4 addresses",
"rate_limit_subnet_len_ipv4_desc": "Subnet prefix length for IPv4 addresses used for rate limiting. The default is 24",
@@ -458,7 +416,6 @@
"list_confirm_delete": "Are you sure you want to delete this list?",
"auto_clients_title": "Runtime clients",
"auto_clients_desc": "Information about IP addresses of devices that are using or may use AdGuard Home. This information is gathered from several sources, including hosts files, reverse DNS, etc.",
"access_title": "Access settings",
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server",
"access_allowed_title": "Allowed clients",
"access_allowed_desc": "A list of CIDRs, IP addresses, or <a>ClientIDs</a>. If this list has entries, AdGuard Home will accept requests only from these clients.",
@@ -576,10 +533,6 @@
"rewrite_domain_name": "Domain name: add a CNAME record",
"rewrite_A": "<0>A</0>: special value, keep <0>A</0> records from the upstream",
"rewrite_AAAA": "<0>AAAA</0>: special value, keep <0>AAAA</0> records from the upstream",
"disable_ipv6": "Disable resolving of IPv6 addresses",
"disable_ipv6_desc": "Drop all DNS queries for IPv6 addresses (type AAAA) and remove IPv6 hints from HTTPS responses.",
"fastest_addr": "Fastest IP address",
"fastest_addr_desc": "Wait for responses from <b>all</b> DNS servers, measure the TCP connection speed for each server, and return the IP address of the server with the fastest connection speed.<br/>This mode can significantly slow down DNS queries, if one or more upstream servers are not responding. Make sure that your upstream servers are stable and your upstream timeout is low.",
"autofix_warning_text": "If you click \"Fix\", AdGuard Home will configure your system to use AdGuard Home DNS server.",
"autofix_warning_list": "It will perform these tasks: <0>Deactivate system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf with /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>",
"autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuard Home by default.",
@@ -617,8 +570,6 @@
"confirm_static_ip": "AdGuard Home will configure {{ip}} to be your static IP address. Do you want to proceed?",
"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).",
"validated_with_dnssec": "Validated with DNSSEC",
"all_queries": "All queries",
"show_blocked_responses": "Blocked",
@@ -668,7 +619,6 @@
"anonymizer_notification": "<0>Note:</0> IP anonymization is enabled. You can disable it in <1>General settings</1>.",
"confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?",
"cache_cleared": "DNS cache successfully cleared",
"clear_cache": "Clear cache",
"make_static": "Make static",
"theme_auto_desc": "Auto (based on the color scheme of your device)",
"theme_dark_desc": "Dark theme",
@@ -829,5 +779,111 @@
"encryption_certificate_name_error": "The server certificate does not include the server name <strong>%value%</strong>",
"encryption_invalid_data": "Invalid data",
"encryption_certificate_no_ip": "This certificate does not contain IP addresses. DDR and DNS-over-TLS may not work properly",
"encryption_certificate_self_signed": "This is a self-signed certificate — it may not work on all devices. If you want to use it, make sure that all your devices will accept it"
"encryption_certificate_self_signed": "This is a self-signed certificate — it may not work on all devices. If you want to use it, make sure that all your devices will accept it",
"dns_settings": "DNS settings",
"upstream_dns": "Upstream DNS servers",
"upstream_dns_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
"upstream_dns_addresses": "Server addresses, one per line",
"upstream_dns_addresses_faq": "View tips for <a>configuring upstream DNS servers</a> and our list of <b>trusted DNS providers</b>",
"upstream_dns_load_balancing": "Load-balancing",
"upstream_dns_load_balancing_desc": "Query one upstream server at a time AdGuard Home uses a weighted random algorithm to select servers with the lowest number of failed lookups and the lowest average lookup time",
"upstream_dns_parallel_requests": "Parallel requests",
"upstream_dns_parallel_requests_desc": "Use parallel queries to speed up resolving by querying all upstream servers simultaneously",
"upstream_dns_fastest_addr": "Fastest IP address",
"upstream_dns_fastest_addr_desc": "Wait for responses from <strong>all</strong> DNS servers, measure the TCP connection speed for each server, and return the IP address of the server with the fastest connection speed",
"upstream_dns_fastest_addr_warning": "This mode can significantly slow down DNS queries, if one or more upstream servers are not responding. Make sure that your upstream servers are stable and your upstream timeout is low",
"upstream_examples_title": "Examples",
"upstream_example_udp": "<code>%ipv4%</code>,<code>%ipv6%</code>: regular DNS (over UDP)",
"upstream_example_udp_port": "<code>%ipv4%</code>,<code>%ipv6%</code>: regular DNS (over UDP, with port)",
"upstream_example_udp_hostname": "<code>%value%</code>: regular DNS (over UDP, hostname)",
"upstream_example_tcp": "<code>%ipv4%</code>,<code>%ipv6%</code>: regular DNS (over TCP)",
"upstream_example_tcp_port": "<code>%ipv4%</code>,<code>%ipv6%</code>: regular DNS (over TCP, with port)",
"upstream_example_tcp_hostname": "<code>%value%</code>: regular DNS (over TCP, hostname)",
"upstream_example_upstream_dot": "<code>%value%</code>: encrypted <a>DNS-over-TLS</a>",
"upstream_example_upstream_doh": "<code>%value%</code>: encrypted <a>DNS-over-HTTPS</a>",
"upstream_example_upstream_doh3": "<code>%value%</code>: encrypted DNS-over-HTTPS with forced <strong>HTTP/3</strong> and no fallback to HTTP/2",
"upstream_example_upstream_doq": "<code>%value%</code>: encrypted <a>DNS-over-QUIC</a>",
"upstream_example_upstream_sdns": "<code>%value%</code>: <a>DNS Stamps</a> for <b>DNSCrypt</b> or <c>DNS-over-HTTPS</c> resolvers",
"upstream_example_upstream_reserved": "<code>%value%</code>: an upstream <a>for specific domains</a>",
"upstream_example_multiple_upstreams_reserved": "<code>%value%</code>: multiple upstreams <a>for specific domains</a>",
"upstream_example_upstream_comment": "<code>%value%</code>: a comment.",
"upstream_fallback_title": "Fallback DNS servers, one per line",
"upstream_fallback_title_faq": "Fallback DNS servers to use when upstream DNS servers are unavailable. Use the same syntax as for the upstream DNS servers",
"ip_addresses_placeholder": "Enter IP addresses, one per line",
"upstream_bootstrap_dns_title": "Bootstrap DNS servers, one per line",
"upstream_bootstrap_dns_faq": "IP addresses of DNS servers used to resolve the IP addresses of DoH/DoT upstream resolvers. Comments are not allowed",
"upstream_ptr": "Private reverse DNS servers, one per line",
"upstream_ptr_faq_1": "AdGuard Home uses these DNS servers for private PTR, SOA, and NS requests. Private requests come from clients with a private IP address and ask for an ARPA domain that contains a subnet from a private IP address range, such as %value%",
"upstream_ptr_faq_2": "If this field is empty, the default DNS resolvers of your OS will be used, except for the AdGuard Home IP addresses",
"upstream_ptr_faq_3": "By default, AdGuard Home uses the following reverse DNS resolvers: %value_1% and %value_2%",
"upstream_private_ptr_resolvers_title": "Use private reverse DNS resolvers",
"upstream_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, and so on. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN",
"upstream_enable_reverse_lookup_title": "Enable reverse lookup of client IP addresses",
"upstream_enable_reverse_lookup_desc": "Reverse-resolve client IP addresses to their hostnames by sending PTR queries to appropriate resolvers: private DNS servers for local clients, or upstream servers for clients with public IP addresses",
"upstream_timeout": "Upstream timeout, in seconds",
"upstream_timeout_faq": "Time to wait for a response from the upstream server",
"upstream_timeout_placeholder": "Enter the upstream server timeout duration",
"apply": "Apply",
"test_upstreams": "Test upstreams",
"server_config_title": "DNS server configuration",
"server_config_rate_limit": "Rate limit, RPS",
"server_config_rate_limit_faq": "The number of requests per second allowed per client. A value of 0 means no limit",
"server_config_rate_limit_placeholder": "Enter rate limit",
"server_config_subnet_len_ipv4": "Subnet prefix length for IPv4 addresses",
"server_config_subnet_len_ipv4_faq": "Subnet prefix length for IPv4 addresses used for rate limiting. The default is 24",
"server_config_subnet_len_ipv6": "Subnet prefix length for IPv6 addresses",
"server_config_subnet_len_ipv6_faq": "Subnet prefix length for IPv6 addresses used for rate limiting. The default is 56",
"server_config_subnet_len_placeholder": "Enter prefix length",
"server_config_rate_limit_whitelist": "Rate-limiting allowlist",
"server_config_rate_limit_whitelist_faq": "IP addresses excluded from rate limiting",
"server_config_edns_enable": "Enable EDNS client subnet",
"server_config_edns_cs_desc": "Add the EDNS Client Subnet (ECS) option to upstream requests and log the values sent by the clients in the query log",
"server_config_edns_use_custom_ip": "Use custom IP address for EDNS",
"server_config_edns_use_custom_ip_desc": "Replace the client IP address with the custom IP address below",
"enter_ip_address_placeholder": "Enter IP address",
"server_config_dnssec_enable": "Enable DNSSEC",
"server_config_dnssec_enable_desc": "Set the DNSSEC flag in outbound DNS queries and check the result. Requires a DNSSEC-enabled resolver",
"server_config_disable_ipv6": "Disable resolving of IPv6 addresses",
"server_config_disable_ipv6_desc": "Drop all DNS queries for IPv6 addresses (type AAAA) and remove IPv6 hints from HTTPS responses",
"server_config_blocking_mode": "Blocking mode",
"server_config_default": "Default",
"server_config_blocking_mode_default_desc": "<strong>Default:</strong> Respond with 0.0.0.0 for A and :: for AAAA if blocked by Adblock-style rule. Respond with the IP address specified in the rule if blocked by /etc/hosts-style rule",
"server_config_refused": "REFUSED",
"server_config_blocking_mode_refused_desc": "<strong>REFUSED:</strong> Respond with REFUSED code",
"server_config_nxdomain": "NXDOMAIN",
"server_config_blocking_mode_nxdomain_desc": "<strong>NXDOMAIN:</strong> Respond with NXDOMAIN code",
"server_config_null_ip": "Null IP address",
"server_config_blocking_mode_null_ip_desc": "<strong>Null IP address:</strong> Respond with 0.0.0.0 for A and :: for AAAA",
"server_config_custom_ip": "Custom IP address",
"server_config_blocking_mode_custom_ip_desc": "<strong>Custom IP address:</strong> Respond with a manually set IP address",
"server_config_blocking_mode_ipv4": "Blocking IPv4",
"server_config_blocking_mode_ipv4_placeholder": "Enter IPv4 address",
"server_config_blocking_mode_ipv4_faq": "IP address to return for a blocked A request",
"server_config_blocking_mode_ipv6": "Blocking IPv6",
"server_config_blocking_mode_ipv6_placeholder": "Enter IPv6 address",
"server_config_blocking_mode_ipv6_faq": "IP address to return for a blocked AAAA request",
"server_config_blocking_mode_ttl": "Blocked response TTL, in seconds",
"server_config_blocking_mode_ttl_faq": "Specifies how long clients should cache a filtered response",
"cache_config_title": "DNS cache configuration",
"cache_config_size": "Cache size, in bytes",
"cache_config_size_faq": "DNS cache size. To disable caching, leave empty",
"cache_config_min_ttl": "Override minimum TTL, in seconds",
"cache_config_min_ttl_faq": "The minimum time-to-live (TTL) value for entries in the DNS cache",
"cache_config_min_ttl_placeholder": "Enter minimum TTL",
"cache_config_max_ttl": "Override maximum TTL, in seconds",
"cache_config_max_ttl_faq": "The maximum time-to-live (TTL) value for entries in the DNS cache",
"cache_config_max_ttl_placeholder": "Enter maximum TTL",
"cache_config_optimistic": "Optimistic caching",
"cache_config_optimistic_desc": "AdGuard Home will respond from the cache even if the entries have expired and will try to update them",
"cache_config_clear": "Clear cache",
"cache_enabled": "Enable cache",
"cache_enabled_desc": "Store DNS responses locally",
"access_settings_title": "Access settings",
"access_settings_allowed_title": "Allowed clients, one per line",
"access_settings_allowed_faq": "A list of CIDRs, IP addresses, or <a>ClientIDs</a>. If this field has entries, AdGuard Home will accept requests only from these clients",
"access_settings_disallowed_title": "Disallowed clients, one per line",
"access_settings_disallowed_faq": "A list of CIDRs, IP addresses, or <a>ClientIDs</a>. If this field has entries, AdGuard Home will drop requests from these clients. This field is ignored if there are entries in <strong>Allowed clients</strong>",
"access_settings_blocked_title": "Disallowed domains, one per line",
"access_settings_blocked_faq_1": "AdGuard Home will drop DNS queries matching these domains. Such queries dont appear in the query log. You can enter exact domain names, wildcards, or URL filter rules, such as example.org, *.example.org, or ||example.org^",
"access_settings_blocked_faq_2": "Not to be confused with filters"
}

View File

@@ -7,6 +7,18 @@
&.disabled {
cursor: default;
}
&.center {
align-items: center;
}
&.start {
align-items: flex-start;
}
&.end {
align-items: flex-end;
}
}
.input {

View File

@@ -12,6 +12,7 @@ type Props = ComponentProps<'input'> & {
onClick?: (e: MouseEvent<HTMLElement>) => void;
children?: ReactNode;
plusStyle?: boolean;
verticalAlign?: 'center' | 'start' | 'end';
};
export const Checkbox = ({
@@ -26,8 +27,12 @@ export const Checkbox = ({
overflow,
onClick,
plusStyle,
verticalAlign,
}: Props) => (
<label htmlFor={id} className={cn(s.checkbox, { [s.disabled]: disabled }, className)} onClick={onClick}>
<label
htmlFor={id}
className={cn(s.checkbox, { [s.disabled]: disabled }, s[verticalAlign], className)}
onClick={onClick}>
<input
id={id}
name={name}

View File

@@ -39,7 +39,7 @@ export const Input = forwardRef<HTMLInputElement, Props>(
disabled,
error,
errorMessage,
size,
size = 'large',
autoComplete,
},
ref,

View File

@@ -10,6 +10,18 @@
margin: 0;
padding: 8px 0;
cursor: pointer;
&.center {
align-items: center;
}
&.start {
align-items: flex-start;
}
&.end {
align-items: flex-end;
}
}
.input {
@@ -18,9 +30,12 @@
.text {
max-width: 250px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
}
.description {
margin-top: 2px;
color: var(--default-description-text);
}
.icon {

View File

@@ -1,7 +1,8 @@
import React from 'react';
import React, { forwardRef } from 'react';
import cn from 'clsx';
import { Icon } from 'panel/common/ui/Icon';
import theme from 'panel/lib/theme';
import s from './Radio.module.pcss';
type Props<T> = {
@@ -10,42 +11,46 @@ type Props<T> = {
disabled?: boolean;
handleChange: (e: T) => void;
value: T;
options: { text: string; value: T }[];
options: { text: string; value: T; description?: React.ReactNode }[];
name?: string;
textClassName?: string;
verticalAlign?: 'center' | 'start' | 'end';
};
export const Radio = <T extends number | string | boolean = string>({
className,
wrapClass,
disabled,
handleChange,
value,
options,
name,
}: Props<T>) => (
<div className={cn(s.wrap, wrapClass)}>
{options.map((o) => (
<label
key={`${o.value}`}
htmlFor={name ? `${name}-${o.value}` : String(o.value)}
className={cn(s.radio, className)}>
<input
id={name ? `${name}-${o.value}` : String(o.value)}
type="radio"
className={s.input}
name={name}
onChange={() => handleChange(o.value)}
checked={value === o.value}
disabled={disabled}
/>
<div className={s.handler}>
<Icon
icon={value === o.value ? 'radio_on' : 'radio_off'}
className={cn(s.icon, { [s.active]: value === o.value })}
export const Radio = forwardRef<HTMLDivElement, Props<any>>(
<T extends number | string | boolean = string>(
{ className, wrapClass, disabled, handleChange, value, options, name, textClassName, verticalAlign }: Props<T>,
ref: React.Ref<HTMLDivElement>,
) => (
<div ref={ref} className={cn(s.wrap, wrapClass)}>
{options.map((o) => (
<label
key={`${o.value}`}
htmlFor={name ? `${name}-${o.value}` : String(o.value)}
className={cn(s.radio, className, s[verticalAlign])}>
<input
id={name ? `${name}-${o.value}` : String(o.value)}
type="radio"
className={s.input}
name={name}
onChange={() => handleChange(o.value)}
checked={value === o.value}
disabled={disabled}
/>
</div>
<div className={s.text}>{o.text}</div>
</label>
))}
</div>
<div className={s.handler}>
<Icon
icon={value === o.value ? 'radio_on' : 'radio_off'}
className={cn(s.icon, { [s.active]: value === o.value })}
/>
</div>
<div className={cn(s.text, textClassName)}>
<div>{o.text}</div>
{o.description && <div className={cn(theme.text.t4, s.description)}>{o.description}</div>}
</div>
</label>
))}
</div>
),
);
Radio.displayName = 'Radio';

View File

@@ -5,7 +5,7 @@ import s from './styles.module.pcss';
type Props = ComponentProps<'textarea'> & {
label?: ReactNode;
size?: 'small' | 'large';
size?: 'small' | 'medium' | 'large';
errorMessage?: string;
};

View File

@@ -42,13 +42,19 @@
}
}
&.medium {
min-height: 120px;
}
&.large {
min-height: 200px;
}
}
.label {
display: block;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
font-size: 14px;
line-height: 24px;

View File

@@ -80,11 +80,11 @@ export const i18n = (lang: LocalesType) => {
};
};
const detectedLanguage = (
(typeof window !== 'undefined' && typeof localStorage !== 'undefined' && LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE)) ||
const detectedLanguage = ((typeof window !== 'undefined' &&
typeof localStorage !== 'undefined' &&
LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE)) ||
(typeof navigator !== 'undefined' && (navigator.language as string)) ||
BASE_LOCALE
) as LocalesType;
BASE_LOCALE) as LocalesType;
let currentLanguage: LocalesType = resolveLanguage(detectedLanguage);
@@ -98,6 +98,10 @@ let translator = translate.createReactTranslator<any>(i18n(currentLanguage), Rea
key: 'strong',
createdTag: 'strong',
},
{
key: 'code',
createdTag: 'code',
},
],
});
@@ -131,6 +135,10 @@ const intl = {
key: 'strong',
createdTag: 'strong',
},
{
key: 'code',
createdTag: 'code',
},
],
});
},

View File

@@ -3,7 +3,7 @@
--default-page-background: var(--gray-90);
--hovered-page-background: var(--gray-80);
--pressed-page-background: var(--gray-70);
--fills-backgrounds-page-background-additional: var(--gray-80);
--page-background-additional: var(--gray-80);
/* Cards background */
--default-cards-background: var(--gray-80);
/* Popup background */

View File

@@ -3,7 +3,7 @@
--default-page-background: var(--gray-0);
--hovered-page-background: var(--gray-10);
--pressed-page-background: var(--gray-20);
--fills-backgrounds-page-background-additional: var(--gray-10);
--page-background-additional: var(--gray-10);
/* Cards background */
--default-cards-background: var(--gray-0);
/* Popup background */

View File

@@ -0,0 +1,29 @@
.accordion {
overflow: hidden;
}
.header {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 16px;
border: none;
cursor: pointer;
text-align: left;
background-color: transparent;
outline: none;
}
.title {
color: var(--default-main-text);
}
.arrow {
transition: transform var(--t2);
color: var(--default-gray-icons);
}
.arrowOpen {
transform: rotate(180deg);
}

View File

@@ -0,0 +1,37 @@
import React, { ReactNode, useState } from 'react';
import cn from 'clsx';
import { Icon } from 'panel/common/ui/Icon';
import theme from 'panel/lib/theme';
import s from './Accordion.module.pcss';
type Props = {
title: ReactNode;
children: ReactNode;
defaultOpen?: boolean;
className?: string;
};
export const Accordion = ({ title, children, defaultOpen = false, className }: Props) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggleOpen = () => {
setIsOpen(!isOpen);
};
return (
<div className={cn(s.accordion, className)}>
<button
type="button"
className={s.header}
onClick={toggleOpen}
aria-expanded={isOpen}
aria-controls="accordion-content">
<Icon icon="arrow_bottom" className={cn(s.arrow, { [s.arrowOpen]: isOpen })} />
<span className={cn(s.title, theme.text.t2, theme.text.semibold)}>{title}</span>
</button>
{isOpen && <div id="accordion-content">{children}</div>}
</div>
);
};

View File

@@ -0,0 +1 @@
export { Accordion } from './Accordion';

View File

@@ -10,9 +10,10 @@ import s from './styles.module.pcss';
type Props = {
text: ReactNode;
menuSize?: 'small' | 'large';
spacing?: boolean;
};
export const FaqTooltip = ({ text, menuSize = 'small' }: Props) => {
export const FaqTooltip = ({ text, menuSize = 'small', spacing = false }: Props) => {
return (
<Dropdown
trigger="hover"
@@ -20,6 +21,7 @@ export const FaqTooltip = ({ text, menuSize = 'small' }: Props) => {
<div
className={cn(theme.dropdown.menu, s.menu, {
[s.menu_large]: menuSize === 'large',
[s.menu_spacing]: spacing,
})}>
{text}
</div>

View File

@@ -5,15 +5,33 @@
.menu {
padding: 16px;
background-color: var(--fills-backgrounds-page-background-additional);
background-color: var(--page-background-additional);
@media (min-width: 768px) {
max-width: 280px;
}
&_spacing {
display: flex;
flex-direction: column;
gap: 16px;
}
&_large {
@media (min-width: 768px) {
max-width: 340px;
}
}
a {
color: var(--default-link);
transition: color var(--t2);
text-decoration: underline;
&:hover,
&:focus {
color: var(--hovered-link);
text-decoration: none;
}
}
}

View File

@@ -102,7 +102,7 @@
height: 100%;
z-index: 1502;
padding: 16px 0;
background-color: var(--fills-backgrounds-page-background-additional);
background-color: var(--page-background-additional);
overflow-y: auto;
}

View File

@@ -26,6 +26,7 @@ export const ICONS = {
check: 'check',
dot: 'dot',
attention: 'attention',
label: 'label',
} as const;
export type IconType = keyof typeof ICONS;
@@ -332,13 +333,7 @@ export const Icons = memo(() => (
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 8V14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M12 8V14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M11.997 16.4045C12.009 16.4025 11.997 16.5955 11.997 16.5955"
stroke="currentColor"
@@ -347,6 +342,15 @@ export const Icons = memo(() => (
strokeLinejoin="round"
/>
</symbol>
<symbol id="label" viewBox="0 0 24 24" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C15.9974 9.79193 14.2081 8.00258 12 8Z"
fill="currentColor"
/>
</symbol>
</svg>
));
Icons.displayName = 'Icons';

View File

@@ -6,7 +6,7 @@
&:hover,
&:focus {
background-color: var(--fills-backgrounds-page-background-additional);
background-color: var(--page-background-additional);
}
}

View File

@@ -17,7 +17,7 @@
padding: 16px 0;
overflow-y: auto;
z-index: 1;
background-color: var(--fills-backgrounds-page-background-additional);
background-color: var(--page-background-additional);
}
&::-webkit-scrollbar {

View File

@@ -20,6 +20,7 @@ import { changeLanguage, getDnsStatus, getTimerStatus } from '../../actions';
import { RootState } from '../../initialState';
import s from './styles.module.pcss';
import { DnsSettings } from '../DnsSettings';
type RouteConfig = {
path: string;
@@ -38,6 +39,11 @@ const ROUTES: RouteConfig[] = [
component: Encryption,
exact: true,
},
{
path: '/dns',
component: DnsSettings,
exact: true,
},
];
const App = () => {

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import cn from 'clsx';
import { setAccessList } from 'panel/actions/access';
import { RootState } from 'panel/initialState';
import intl from 'panel/common/intl';
import theme from 'panel/lib/theme';
import { Form } from './Form';
export const Access = () => {
const dispatch = useDispatch();
const { processingSet, ...values } = useSelector((state: RootState) => state.access, shallowEqual);
const handleFormSubmit = (values: any) => {
dispatch(setAccessList(values));
};
return (
<div>
<h2
className={cn(
theme.layout.subtitle,
theme.layout.subtitle_compact,
theme.title.h5,
theme.title.h4_tablet,
)}>
{intl.getMessage('access_settings_title')}
</h2>
<Form initialValues={values} onSubmit={handleFormSubmit} processingSet={processingSet} />
</div>
);
};

View File

@@ -0,0 +1,147 @@
import React, { ReactNode } from 'react';
import { Controller, useForm } from 'react-hook-form';
import intl from 'panel/common/intl';
import { Textarea } from 'panel/common/controls/Textarea';
import { Button } from 'panel/common/ui/Button';
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
import { CLIENT_ID_LINK } from 'panel/helpers/constants';
import { removeEmptyLines, trimMultilineString } from 'panel/helpers/helpers';
import theme from 'panel/lib/theme';
type FormData = {
allowed_clients: string;
disallowed_clients: string;
blocked_hosts: string;
};
const fields: {
id: keyof FormData;
title: string;
faq: ReactNode;
normalizeOnBlur: (value: string) => string;
}[] = [
{
id: 'allowed_clients',
title: intl.getMessage('access_settings_allowed_title'),
faq: intl.getMessage('access_settings_allowed_faq', {
a: (text: string) => (
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
normalizeOnBlur: removeEmptyLines,
},
{
id: 'disallowed_clients',
title: intl.getMessage('access_settings_disallowed_title'),
faq: intl.getMessage('access_settings_disallowed_faq', {
a: (text: string) => (
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
normalizeOnBlur: trimMultilineString,
},
{
id: 'blocked_hosts',
title: intl.getMessage('access_settings_blocked_title'),
faq: (
<>
<div>{intl.getMessage('access_settings_blocked_faq_1')}</div>
<div>{intl.getMessage('access_settings_blocked_faq_2')}</div>
</>
),
normalizeOnBlur: removeEmptyLines,
},
];
type FormProps = {
initialValues?: {
allowed_clients?: string;
disallowed_clients?: string;
blocked_hosts?: string;
};
onSubmit: (data: FormData) => void;
processingSet: boolean;
};
export const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
const {
control,
handleSubmit,
watch,
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onBlur',
defaultValues: {
allowed_clients: initialValues?.allowed_clients || '',
disallowed_clients: initialValues?.disallowed_clients || '',
blocked_hosts: initialValues?.blocked_hosts || '',
},
});
const allowedClients = watch('allowed_clients');
const renderField = ({
id,
title,
faq,
normalizeOnBlur,
}: {
id: keyof FormData;
title: string;
faq: ReactNode;
normalizeOnBlur: (value: string) => string;
}) => {
const disabled = allowedClients && id === 'disallowed_clients';
return (
<div key={id} className={theme.form.input}>
<Controller
name={id}
control={control}
render={({ field }) => (
<Textarea
{...field}
id={id}
data-testid={id}
label={
<>
{title}
{disabled && <>&nbsp;({intl.getMessage('disabled')})</>}
<FaqTooltip text={faq} menuSize="large" spacing={id === 'blocked_hosts'} />
</>
}
disabled={disabled || processingSet}
onBlur={(e) => {
field.onChange(normalizeOnBlur(e.target.value));
}}
size="medium"
/>
)}
/>
</div>
);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className={theme.form.form}>
<div className={theme.form.group}>{fields.map((f) => renderField(f))}</div>
<div className={theme.form.buttonGroup}>
<Button
type="submit"
id="access_save"
variant="primary"
size="small"
disabled={isSubmitting || !isDirty || processingSet}
className={theme.form.button}>
{intl.getMessage('save')}
</Button>
</div>
</form>
);
};

View File

@@ -0,0 +1 @@
export { Access } from './Access';

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import cn from 'clsx';
import { setDnsConfig } from 'panel/actions/dnsConfig';
import { replaceEmptyStringsWithZeroes, replaceZeroWithEmptyString } from 'panel/helpers/helpers';
import { RootState } from 'panel/initialState';
import intl from 'panel/common/intl';
import theme from 'panel/lib/theme';
import { Form } from './Form';
export const Cache = () => {
const dispatch = useDispatch();
const { cache_enabled, cache_size, cache_ttl_max, cache_ttl_min, cache_optimistic } = useSelector(
(state: RootState) => state.dnsConfig,
shallowEqual,
);
const handleFormSubmit = (values: any) => {
const completedFields = replaceEmptyStringsWithZeroes(values);
dispatch(setDnsConfig(completedFields));
};
return (
<div>
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
{intl.getMessage('dns_cache_config')}
</h2>
<Form
initialValues={{
cache_enabled,
cache_size: replaceZeroWithEmptyString(cache_size),
cache_ttl_max: replaceZeroWithEmptyString(cache_ttl_max),
cache_ttl_min: replaceZeroWithEmptyString(cache_ttl_min),
cache_optimistic,
}}
onSubmit={handleFormSubmit}
/>
</div>
);
};

View File

@@ -0,0 +1,194 @@
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import intl from 'panel/common/intl';
import { clearDnsCache } from 'panel/actions/dnsConfig';
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from 'panel/helpers/constants';
import { RootState } from 'panel/initialState';
import { Checkbox } from 'panel/common/controls/Checkbox';
import { Input } from 'panel/common/controls/Input';
import { Button } from 'panel/common/ui/Button';
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
import theme from 'panel/lib/theme';
const INPUTS_FIELDS = [
{
name: CACHE_CONFIG_FIELDS.cache_size,
title: intl.getMessage('cache_config_size'),
faq: intl.getMessage('cache_config_size_faq'),
placeholder: intl.getMessage('enter_cache_size'),
},
{
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
title: intl.getMessage('cache_config_min_ttl'),
faq: intl.getMessage('cache_config_min_ttl_faq'),
placeholder: intl.getMessage('cache_config_min_ttl_placeholder'),
},
{
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
title: intl.getMessage('cache_config_max_ttl'),
faq: intl.getMessage('cache_config_max_ttl_faq'),
placeholder: intl.getMessage('cache_config_max_ttl_placeholder'),
},
];
type FormData = {
cache_enabled: boolean;
cache_size: number;
cache_ttl_min: number;
cache_ttl_max: number;
cache_optimistic: boolean;
};
type CacheFormProps = {
initialValues?: Partial<FormData>;
onSubmit: (data: FormData) => void;
};
export const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
const dispatch = useDispatch();
const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig);
const {
handleSubmit,
watch,
control,
formState: { isSubmitting },
} = useForm<FormData>({
mode: 'onBlur',
defaultValues: {
cache_enabled: initialValues?.cache_enabled || false,
cache_size: initialValues?.cache_size || 0,
cache_ttl_min: initialValues?.cache_ttl_min || 0,
cache_ttl_max: initialValues?.cache_ttl_max || 0,
cache_optimistic: initialValues?.cache_optimistic || false,
},
});
const cache_enabled = watch('cache_enabled');
const cache_size = watch('cache_size');
const cache_ttl_min = watch('cache_ttl_min');
const cache_ttl_max = watch('cache_ttl_max');
const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max;
const cacheSizeZeroWhenEnabled = cache_enabled && cache_size === 0;
const handleClearCache = () => {
if (window.confirm(intl.getMessage('confirm_dns_cache_clear'))) {
dispatch(clearDnsCache());
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className={theme.form.form}>
<div className={theme.form.group}>
<div className={theme.form.input}>
<Controller
name="cache_enabled"
control={control}
render={({ field }) => (
<Checkbox
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
data-testid="dns_cache_enabled"
disabled={processingSetConfig}
verticalAlign="start">
<div>
<div className={theme.text.t2}>{intl.getMessage('cache_enabled')}</div>
<div className={theme.text.t4}>{intl.getMessage('cache_enabled_desc')}</div>
</div>
</Checkbox>
)}
/>
</div>
{INPUTS_FIELDS.map(({ name, title, faq, placeholder }) => (
<div key={name} className={theme.form.input}>
<Controller
name={name as keyof FormData}
control={control}
render={({ field }) => (
<Input
{...field}
type="number"
id={name}
label={
<>
{title}
<FaqTooltip text={faq} menuSize="large" />
</>
}
placeholder={placeholder}
disabled={processingSetConfig}
min={0}
max={UINT32_RANGE.MAX}
onChange={(e) => {
const value = e.target.value === '' ? 0 : Number(e.target.value);
field.onChange(value);
}}
value={field.value === 0 ? '' : String(field.value)}
/>
)}
/>
{name === CACHE_CONFIG_FIELDS.cache_size && cacheSizeZeroWhenEnabled && (
<span className={theme.form.error}>{intl.getMessage('cache_size_validation')}</span>
)}
</div>
))}
{minExceedsMax && <div className={theme.form.error}>{intl.getMessage('ttl_cache_validation')}</div>}
<div className={theme.form.input}>
<Controller
name="cache_optimistic"
control={control}
render={({ field }) => (
<Checkbox
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
data-testid="dns_cache_optimistic"
disabled={processingSetConfig}
verticalAlign="start">
<div>
<div className={theme.text.t2}>{intl.getMessage('cache_config_optimistic')}</div>
<div className={theme.text.t4}>
{intl.getMessage('cache_config_optimistic_desc')}
</div>
</div>
</Checkbox>
)}
/>
</div>
</div>
<div className={theme.form.buttonGroup}>
<Button
type="submit"
id="dns_save"
variant="primary"
size="small"
disabled={isSubmitting || processingSetConfig || minExceedsMax || cacheSizeZeroWhenEnabled}
className={theme.form.button}>
{intl.getMessage('save')}
</Button>
<Button
type="button"
id="dns_clear"
variant="secondary-danger"
size="small"
onClick={handleClearCache}
className={theme.form.button}>
{intl.getMessage('cache_config_clear')}
</Button>
</div>
</form>
);
};

View File

@@ -0,0 +1 @@
export { Cache } from './Cache';

View File

@@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getDnsConfig } from 'panel/actions/dnsConfig';
import { getAccessList } from 'panel/actions/access';
import { RootState } from 'panel/initialState';
import intl from 'panel/common/intl';
import cn from 'clsx';
import { PageLoader } from 'panel/common/ui/Loader';
import theme from 'panel/lib/theme';
import { Upstream } from './Upstream';
import { Access } from './Access';
import { ServerConfig } from './ServerConfig';
import { Cache } from './Cache';
export const DnsSettings = () => {
const dispatch = useDispatch();
const processing = useSelector((state: RootState) => state.access.processing);
const processingGetConfig = useSelector((state: RootState) => state.dnsConfig.processingGetConfig);
const isDataLoading = processing || processingGetConfig;
useEffect(() => {
dispatch(getAccessList());
dispatch(getDnsConfig());
}, []);
return (
<div className={theme.layout.container}>
<h1 className={cn(theme.layout.title, theme.title.h4, theme.title.h3_tablet)}>
{intl.getMessage('dns_settings')}
</h1>
{isDataLoading ? (
<PageLoader />
) : (
<>
<Upstream />
<ServerConfig />
<Cache />
<Access />
</>
)}
</div>
);
};

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import cn from 'clsx';
import { setDnsConfig } from 'panel/actions/dnsConfig';
import { RootState } from 'panel/initialState';
import intl from 'panel/common/intl';
import theme from 'panel/lib/theme';
import { Form } from './blocks/Form';
export const ServerConfig = () => {
const dispatch = useDispatch();
const {
blocking_mode,
ratelimit,
ratelimit_subnet_len_ipv4,
ratelimit_subnet_len_ipv6,
ratelimit_whitelist,
blocking_ipv4,
blocking_ipv6,
blocked_response_ttl,
edns_cs_enabled,
edns_cs_use_custom,
edns_cs_custom_ip,
dnssec_enabled,
disable_ipv6,
processingSetConfig,
} = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
const handleFormSubmit = (values: any) => {
dispatch(setDnsConfig(values));
};
return (
<div>
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
{intl.getMessage('dns_config')}
</h2>
<Form
initialValues={{
ratelimit,
ratelimit_subnet_len_ipv4,
ratelimit_subnet_len_ipv6,
ratelimit_whitelist,
blocking_mode,
blocking_ipv4,
blocking_ipv6,
blocked_response_ttl,
edns_cs_enabled,
disable_ipv6,
dnssec_enabled,
edns_cs_use_custom,
edns_cs_custom_ip,
}}
onSubmit={handleFormSubmit}
processing={processingSetConfig}
/>
</div>
);
};

View File

@@ -0,0 +1,7 @@
.subtitle {
padding-top: 24px;
}
.descriptions {
padding: 12px 0 8px;
}

View File

@@ -0,0 +1,462 @@
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import cn from 'clsx';
import intl from 'panel/common/intl';
import { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from 'panel/helpers/validators';
import { BLOCKING_MODES, UINT32_RANGE } from 'panel/helpers/constants';
import { Checkbox } from 'panel/common/controls/Checkbox';
import { Input } from 'panel/common/controls/Input';
import { toNumber } from 'panel/helpers/form';
import { Textarea } from 'panel/common/controls/Textarea';
import { Radio } from 'panel/common/controls/Radio';
import { Button } from 'panel/common/ui/Button';
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
import theme from 'panel/lib/theme';
import s from './Form.module.pcss';
const blockingModesDescriptions = [
intl.getMessage('server_config_blocking_mode_default_desc'),
intl.getMessage('server_config_blocking_mode_refused_desc'),
intl.getMessage('server_config_blocking_mode_nxdomain_desc'),
intl.getMessage('server_config_blocking_mode_null_ip_desc'),
intl.getMessage('server_config_blocking_mode_custom_ip_desc'),
];
const checkboxes: {
name: 'dnssec_enabled' | 'disable_ipv6';
placeholder: string;
subtitle: string;
}[] = [
{
name: 'dnssec_enabled',
placeholder: intl.getMessage('server_config_dnssec_enable'),
subtitle: intl.getMessage('server_config_dnssec_enable_desc'),
},
{
name: 'disable_ipv6',
placeholder: intl.getMessage('server_config_disable_ipv6'),
subtitle: intl.getMessage('server_config_disable_ipv6_desc'),
},
];
const customIps: {
name: 'blocking_ipv4' | 'blocking_ipv6';
label: string;
placeholder: string;
faq: string;
validateIp: (value: string) => string;
}[] = [
{
name: 'blocking_ipv4',
label: intl.getMessage('server_config_blocking_mode_ipv4'),
placeholder: intl.getMessage('server_config_blocking_mode_ipv4_placeholder'),
faq: intl.getMessage('server_config_blocking_mode_ipv4_faq'),
validateIp: validateIpv4,
},
{
name: 'blocking_ipv6',
label: intl.getMessage('server_config_blocking_mode_ipv6'),
placeholder: intl.getMessage('server_config_blocking_mode_ipv6_placeholder'),
faq: intl.getMessage('server_config_blocking_mode_ipv6_faq'),
validateIp: validateIpv6,
},
];
const blockingModeOptions = [
{
value: BLOCKING_MODES.default,
text: intl.getMessage('server_config_default'),
},
{
value: BLOCKING_MODES.refused,
text: intl.getMessage('server_config_refused'),
},
{
value: BLOCKING_MODES.nxdomain,
text: intl.getMessage('server_config_nxdomain'),
},
{
value: BLOCKING_MODES.null_ip,
text: intl.getMessage('server_config_null_ip'),
},
{
value: BLOCKING_MODES.custom_ip,
text: intl.getMessage('server_config_custom_ip'),
},
];
type FormData = {
ratelimit: number;
ratelimit_subnet_len_ipv4: number;
ratelimit_subnet_len_ipv6: number;
ratelimit_whitelist: string;
edns_cs_enabled: boolean;
edns_cs_use_custom: boolean;
edns_cs_custom_ip?: string;
dnssec_enabled: boolean;
disable_ipv6: boolean;
blocking_mode: string;
blocking_ipv4?: string;
blocking_ipv6?: string;
blocked_response_ttl: number;
};
type Props = {
processing?: boolean;
initialValues?: Partial<FormData>;
onSubmit: (data: FormData) => void;
};
export const Form = ({ processing, initialValues, onSubmit }: Props) => {
const {
handleSubmit,
watch,
control,
formState: { isSubmitting },
} = useForm<FormData>({
mode: 'onBlur',
defaultValues: initialValues,
});
const blocking_mode = watch('blocking_mode');
const edns_cs_enabled = watch('edns_cs_enabled');
const edns_cs_use_custom = watch('edns_cs_use_custom');
return (
<form onSubmit={handleSubmit(onSubmit)} className={theme.form.form}>
<div className={theme.form.group}>
<div className={theme.form.input}>
<Controller
name="ratelimit"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_ratelimit"
type="number"
label={
<>
{intl.getMessage('server_config_rate_limit')}
<FaqTooltip
text={intl.getMessage('server_config_rate_limit_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('server_config_rate_limit_placeholder')}
errorMessage={fieldState.error?.message}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
disabled={!!processing}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="ratelimit_subnet_len_ipv4"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_subnet_ipv4"
type="number"
label={
<>
{intl.getMessage('server_config_subnet_len_ipv4')}
<FaqTooltip
text={intl.getMessage('server_config_subnet_len_ipv4_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('server_config_subnet_len_placeholder')}
errorMessage={fieldState.error?.message}
min={0}
max={32}
disabled={!!processing}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="ratelimit_subnet_len_ipv6"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_subnet_ipv6"
type="number"
label={
<>
{intl.getMessage('server_config_subnet_len_ipv6')}
<FaqTooltip
text={intl.getMessage('server_config_subnet_len_ipv6_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('server_config_subnet_len_placeholder')}
errorMessage={fieldState.error?.message}
min={0}
max={128}
disabled={!!processing}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="ratelimit_whitelist"
control={control}
render={({ field, fieldState }) => (
<Textarea
{...field}
data-testid="dns_config_ratelimit_whitelist"
label={
<>
{intl.getMessage('server_config_rate_limit_whitelist')}
<FaqTooltip
text={intl.getMessage('server_config_rate_limit_whitelist_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
errorMessage={fieldState.error?.message}
disabled={!!processing}
size="medium"
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="edns_cs_enabled"
control={control}
render={({ field }) => (
<Checkbox
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
data-testid="dns_config_edns_cs_enabled"
disabled={!!processing}
verticalAlign="start">
<div>
<div className={theme.text.t2}>{intl.getMessage('server_config_edns_enable')}</div>
<div className={theme.text.t4}>{intl.getMessage('server_config_edns_cs_desc')}</div>
</div>
</Checkbox>
)}
/>
</div>
<div className={theme.form.inner}>
<div className={theme.form.input}>
<Controller
name="edns_cs_use_custom"
control={control}
render={({ field }) => (
<Checkbox
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
data-testid="dns_config_edns_use_custom_ip"
disabled={processing || !edns_cs_enabled}
verticalAlign="start">
<div>
<div className={theme.text.t2}>
{intl.getMessage('server_config_edns_use_custom_ip')}
</div>
<div className={theme.text.t4}>
{intl.getMessage('server_config_edns_use_custom_ip_desc')}
</div>
</div>
</Checkbox>
)}
/>
</div>
{edns_cs_use_custom && (
<div className={theme.form.input}>
<Controller
name="edns_cs_custom_ip"
control={control}
rules={{
validate: {
required: validateRequiredValue,
id: validateIp,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_edns_cs_custom_ip"
placeholder={intl.getMessage('enter_ip_address_placeholder')}
errorMessage={fieldState.error?.message}
disabled={processing || !edns_cs_enabled}
/>
)}
/>
</div>
)}
</div>
{checkboxes.map(({ name, placeholder, subtitle }) => (
<div key={name} className={theme.form.input}>
<Controller
name={name}
control={control}
render={({ field }) => (
<Checkbox
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
id={`dns_config_${name}`}
disabled={!!processing}
verticalAlign="start">
<div>
<div className={theme.text.t2}>{placeholder}</div>
<div className={theme.text.t4}>{subtitle}</div>
</div>
</Checkbox>
)}
/>
</div>
))}
<div className={theme.form.input}>
<div className={cn(s.subtitle, theme.title.h6)}>
{intl.getMessage('server_config_blocking_mode')}
</div>
<div className={s.descriptions}>
{blockingModesDescriptions.map((description, index) => (
<div className={theme.text.t2} key={index}>
{description}
</div>
))}
</div>
<Controller
name="blocking_mode"
control={control}
render={({ field }) => (
<Radio
value={field.value}
handleChange={field.onChange}
name={field.name}
options={blockingModeOptions}
disabled={!!processing}
/>
)}
/>
</div>
{blocking_mode === BLOCKING_MODES.custom_ip && (
<>
{customIps.map(({ label, name, placeholder, faq, validateIp }) => (
<div key={name} className={theme.form.input}>
<Controller
name={name}
control={control}
rules={{
validate: {
required: validateRequiredValue,
ip: validateIp,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid={`dns_config_${name}`}
type="text"
label={
<>
{label}
<FaqTooltip text={faq} menuSize="large" />
</>
}
placeholder={placeholder}
errorMessage={fieldState.error?.message}
disabled={!!processing}
/>
)}
/>
</div>
))}
</>
)}
<div className={theme.form.input}>
<Controller
name="blocked_response_ttl"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_blocked_response_ttl"
type="number"
label={
<>
{intl.getMessage('server_config_blocking_mode_ttl')}
<FaqTooltip
text={intl.getMessage('server_config_blocking_mode_ttl_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('form_enter_blocked_response_ttl')}
errorMessage={fieldState.error?.message}
min={UINT32_RANGE.MIN}
max={UINT32_RANGE.MAX}
disabled={!!processing}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
</div>
<div className={theme.form.buttonGroup}>
<Button
type="submit"
id="dns_config_save"
variant="primary"
disabled={isSubmitting || processing}
className={theme.form.button}
size="small">
{intl.getMessage('save')}
</Button>
</div>
</form>
);
};

View File

@@ -0,0 +1 @@
export { Form } from './Form';

View File

@@ -0,0 +1 @@
export { ServerConfig } from './ServerConfig';

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import cn from 'clsx';
import intl from 'panel/common/intl';
import { setDnsConfig } from 'panel/actions/dnsConfig';
import { RootState } from 'panel/initialState';
import theme from 'panel/lib/theme';
import { Form } from './blocks/Form';
type UpstreamFormData = {
upstream_dns: string;
upstream_mode: string;
fallback_dns: string;
bootstrap_dns: string;
local_ptr_upstreams: string;
use_private_ptr_resolvers: boolean;
resolve_clients: boolean;
upstream_timeout: number;
};
export const Upstream = () => {
const dispatch = useDispatch();
const {
upstream_dns,
fallback_dns,
bootstrap_dns,
upstream_mode,
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
upstream_timeout,
} = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);
const handleSubmit = (values: UpstreamFormData) => {
const {
fallback_dns,
bootstrap_dns,
upstream_dns,
upstream_mode,
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
upstream_timeout,
} = values;
const dnsConfig = {
fallback_dns,
bootstrap_dns,
upstream_mode,
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
upstream_timeout,
...(upstream_dns_file ? null : { upstream_dns }),
};
dispatch(setDnsConfig(dnsConfig));
};
const upstreamDns = upstream_dns_file
? intl.getMessage('upstream_dns_configured_in_file', { path: upstream_dns_file })
: upstream_dns;
return (
<div>
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
{intl.getMessage('upstream_dns')}
</h2>
<Form
initialValues={{
upstream_dns: upstreamDns,
fallback_dns,
bootstrap_dns,
upstream_mode,
resolve_clients,
local_ptr_upstreams,
use_private_ptr_resolvers,
upstream_timeout,
}}
onSubmit={handleSubmit}
/>
</div>
);
};

View File

@@ -0,0 +1,35 @@
.list {
margin: 0;
padding: 0;
code {
display: inline-block;
vertical-align: middle;
padding: 2px 4px;
background-color: var(--page-background-additional);
font-family: inherit;
border: 1px solid var(--default-item-divider);
border-radius: 4px;
}
a {
color: var(--default-link);
transition: color var(--t2);
text-decoration: underline;
&:hover,
&:focus {
color: var(--hovered-link);
text-decoration: none;
}
}
}
.listItem {
padding: 8px 16px;
}
.icon {
margin-right: 8px;
color: var(--default-black-icons);
}

View File

@@ -0,0 +1,124 @@
import React from 'react';
import cn from 'clsx';
import intl from 'panel/common/intl';
import { COMMENT_LINE_DEFAULT_TOKEN } from 'panel/helpers/constants';
import { Accordion } from 'panel/common/ui/Accordion';
import theme from 'panel/lib/theme';
import { Icon } from 'panel/common/ui/Icon';
import s from './Examples.module.pcss';
const examples = [
intl.getMessage('upstream_example_udp', {
ipv4: '94.140.14.140',
ipv6: '2a10:50c0::1:ff',
}),
intl.getMessage('upstream_example_udp_port', {
ipv4: '94.140.14.140:53',
ipv6: '[2a10:50c0::1:ff]:53',
}),
intl.getMessage('upstream_example_udp_hostname', {
value: 'udp://unfiltered.adguard-dns.com',
}),
intl.getMessage('upstream_example_tcp', {
ipv4: 'tcp://94.140.14.140',
ipv6: 'tcp://[2a10:50c0::1:ff]',
}),
intl.getMessage('upstream_example_tcp_port', {
ipv4: 'tcp://94.140.14.140:53',
ipv6: 'tcp://[2a10:50c0::1:ff]:53',
}),
intl.getMessage('upstream_example_tcp_hostname', {
value: 'tcp://unfiltered.adguard-dns.com',
}),
intl.getMessage('upstream_example_upstream_dot', {
value: 'tls://unfiltered.adguard-dns.com',
a: (text: string) => (
<a href="https://en.wikipedia.org/wiki/DNS_over_TLS" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_upstream_doh', {
value: 'https://unfiltered.adguard-dns.com/dns-query',
a: (text: string) => (
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_upstream_doh3', {
value: 'h3://unfiltered.adguard-dns.com/dns-query',
a: (text: string) => (
<a href="https://en.wikipedia.org/wiki/HTTP/3" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_upstream_doq', {
value: 'quic://unfiltered.adguard-dns.com',
a: (text: string) => (
<a href="https://datatracker.ietf.org/doc/html/rfc9250" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_upstream_sdns', {
value: 'sdns://...',
a: (text: string) => (
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
b: (text: string) => (
<a href="https://dnscrypt.info/" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
c: (text: string) => (
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_upstream_reserved', {
value: '[/example.local/]94.140.14.140',
a: (text: string) => (
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
target="_blank"
rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_multiple_upstreams_reserved', {
value: '[/example.local/]94.140.14.140 2a10:50c0::1:ff',
a: (text: string) => (
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
target="_blank"
rel="noopener noreferrer">
{text}
</a>
),
}),
intl.getMessage('upstream_example_upstream_comment', {
value: `${COMMENT_LINE_DEFAULT_TOKEN} comment`,
}),
];
export const Examples = () => (
<Accordion title={intl.getMessage('upstream_examples_title')} defaultOpen>
<div className={s.list}>
{examples.map((example, index) => (
<div key={index} className={cn(theme.text.t3, s.listItem)}>
<Icon icon="label" className={s.icon} />
{example}
</div>
))}
</div>
</Accordion>
);

View File

@@ -0,0 +1 @@
export { Examples } from './Examples';

View File

@@ -0,0 +1,8 @@
.radioText {
max-width: 100%;
}
.warning {
margin-top: 8px;
color: var(--default-attention-link);
}

View File

@@ -0,0 +1,377 @@
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { testUpstreamWithFormValues } from 'panel/actions';
import { Textarea } from 'panel/common/controls/Textarea';
import { Input } from 'panel/common/controls/Input';
import { Radio } from 'panel/common/controls/Radio';
import { Checkbox } from 'panel/common/controls/Checkbox';
import { Button } from 'panel/common/ui/Button';
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
import { toNumber } from 'panel/helpers/form';
import intl from 'panel/common/intl';
import { DNS_REQUEST_OPTIONS, UINT32_RANGE } from 'panel/helpers/constants';
import theme from 'panel/lib/theme';
import { RootState } from 'panel/initialState';
import { Examples } from '../Examples';
import s from './Form.module.pcss';
type FormData = {
upstream_dns: string;
upstream_mode: string;
fallback_dns: string;
bootstrap_dns: string;
local_ptr_upstreams: string;
use_private_ptr_resolvers: boolean;
resolve_clients: boolean;
upstream_timeout: number;
};
type FormProps = {
initialValues?: Partial<FormData>;
onSubmit: (data: FormData) => void;
};
const upstreamModeOptions = [
{
text: intl.getMessage('upstream_dns_load_balancing'),
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
description: intl.getMessage('upstream_dns_load_balancing_desc'),
},
{
text: intl.getMessage('upstream_dns_parallel_requests'),
value: DNS_REQUEST_OPTIONS.PARALLEL,
description: intl.getMessage('upstream_dns_parallel_requests_desc'),
},
{
text: intl.getMessage('upstream_dns_fastest_addr'),
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
description: (
<>
{intl.getMessage('upstream_dns_fastest_addr_desc')}
<div className={s.warning}>{intl.getMessage('upstream_dns_fastest_addr_warning')}</div>
</>
),
},
];
export const Form = ({ initialValues, onSubmit }: FormProps) => {
const dispatch = useDispatch();
const {
control,
handleSubmit,
watch,
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onBlur',
defaultValues: {
upstream_dns: initialValues?.upstream_dns || '',
upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,
fallback_dns: initialValues?.fallback_dns || '',
bootstrap_dns: initialValues?.bootstrap_dns || '',
local_ptr_upstreams: initialValues?.local_ptr_upstreams || '',
use_private_ptr_resolvers: initialValues?.use_private_ptr_resolvers || false,
resolve_clients: initialValues?.resolve_clients || false,
upstream_timeout: initialValues?.upstream_timeout || 0,
},
});
const upstream_dns = watch('upstream_dns');
const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);
const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);
const defaultLocalPtrUpstreams = useSelector((state: RootState) => state.dnsConfig.default_local_ptr_upstreams);
const upstreamDnsFile = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);
const handleUpstreamTest = () => {
const formValues = {
bootstrap_dns: watch('bootstrap_dns'),
upstream_dns: watch('upstream_dns'),
local_ptr_upstreams: watch('local_ptr_upstreams'),
fallback_dns: watch('fallback_dns'),
};
dispatch(testUpstreamWithFormValues(formValues));
};
const isSavingDisabled = () => {
return isSubmitting || !isDirty || processingSetConfig || processingTestUpstream;
};
const isTestDisabled = () => {
return !upstream_dns || processingTestUpstream;
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className={theme.form.group}>
<div className={theme.form.input}>
<Controller
name="upstream_dns"
control={control}
render={({ field }) => (
<>
<Textarea
{...field}
id="upstream_dns"
label={
<>
{intl.getMessage('upstream_dns_addresses')}
<FaqTooltip
text={intl.getMessage('upstream_dns_addresses_faq', {
a: (text: string) => (
<a
href="https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}>
{text}
</a>
),
b: (text: string) => (
<a
href="https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}>
{text}
</a>
),
})}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('upstream_dns_placeholder')}
disabled={!!upstreamDnsFile || processingSetConfig || processingTestUpstream}
size="medium"
/>
</>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="upstream_mode"
control={control}
render={({ field }) => (
<Radio
{...field}
handleChange={field.onChange}
options={upstreamModeOptions}
disabled={processingSetConfig || processingTestUpstream}
verticalAlign="start"
textClassName={s.radioText}
/>
)}
/>
</div>
</div>
<Examples />
<div className={theme.form.group}>
<div className={theme.form.input}>
<Controller
name="fallback_dns"
control={control}
render={({ field }) => (
<Textarea
{...field}
id="fallback_dns"
label={
<>
{intl.getMessage('upstream_fallback_title')}
<FaqTooltip
text={intl.getMessage('upstream_fallback_title_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
disabled={processingSetConfig}
size="medium"
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="bootstrap_dns"
control={control}
render={({ field }) => (
<Textarea
{...field}
id="bootstrap_dns"
data-testid="bootstrap_dns"
label={
<>
{intl.getMessage('upstream_bootstrap_dns_title')}
<FaqTooltip
text={intl.getMessage('upstream_bootstrap_dns_faq')}
menuSize="large"
/>
</>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
disabled={processingSetConfig}
size="medium"
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="local_ptr_upstreams"
control={control}
render={({ field }) => (
<Textarea
{...field}
id="local_ptr_upstreams"
data-testid="local_ptr_upstreams"
label={
<>
{intl.getMessage('upstream_ptr')}
<FaqTooltip
text={
<>
<div>
{intl.getMessage('upstream_ptr_faq_1', {
value: '192.168.1.1/24',
})}
</div>
<div>{intl.getMessage('upstream_ptr_faq_2')}</div>
{defaultLocalPtrUpstreams?.length > 0 && (
<div>
{intl.getMessage('upstream_ptr_faq_3', {
value_1: defaultLocalPtrUpstreams[0],
value_2: defaultLocalPtrUpstreams[1] || '',
})}
</div>
)}
</>
}
menuSize="large"
spacing
/>
</>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
disabled={processingSetConfig}
size="medium"
/>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="use_private_ptr_resolvers"
control={control}
render={({ field }) => (
<Checkbox
id="dns_use_private_ptr_resolvers"
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
disabled={processingSetConfig}
verticalAlign="start">
<div>
<div className={theme.text.t2}>
{intl.getMessage('upstream_private_ptr_resolvers_title')}
</div>
<div className={theme.text.t4}>
{intl.getMessage('upstream_private_ptr_resolvers_desc')}
</div>
</div>
</Checkbox>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="resolve_clients"
control={control}
render={({ field }) => (
<Checkbox
id="dns_resolve_clients"
name={field.name}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
disabled={processingSetConfig}
verticalAlign="start">
<div>
<div className={theme.text.t2}>
{intl.getMessage('upstream_enable_reverse_lookup_title')}
</div>
<div className={theme.text.t4}>
{intl.getMessage('upstream_enable_reverse_lookup_desc')}
</div>
</div>
</Checkbox>
)}
/>
</div>
<div className={theme.form.input}>
<Controller
name="upstream_timeout"
control={control}
render={({ field }) => (
<Input
{...field}
type="number"
id="upstream_timeout"
label={
<>
{intl.getMessage('upstream_timeout')}
<FaqTooltip text={intl.getMessage('upstream_timeout_faq')} menuSize="large" />
</>
}
placeholder={intl.getMessage('upstream_timeout_placeholder')}
disabled={processingSetConfig}
min={1}
max={UINT32_RANGE.MAX}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
</div>
<div className={theme.form.buttonGroup}>
<Button
type="submit"
variant="primary"
size="small"
id="dns_upstream_save"
disabled={isSavingDisabled()}
className={theme.form.button}>
{intl.getMessage('apply')}
</Button>
<Button
type="button"
variant="secondary"
size="small"
id="dns_upstream_test"
onClick={handleUpstreamTest}
disabled={isTestDisabled()}
className={theme.form.button}>
{intl.getMessage('test_upstreams')}
</Button>
</div>
</form>
);
};

View File

@@ -0,0 +1 @@
export { Form } from './Form';

View File

@@ -0,0 +1 @@
export { Upstream } from './Upstream';

View File

@@ -0,0 +1 @@
export { DnsSettings } from './DnsSettings';

View File

@@ -6,7 +6,7 @@
&:hover,
&:focus {
background-color: var(--fills-backgrounds-page-background-additional);
background-color: var(--page-background-additional);
}
}

View File

@@ -321,6 +321,7 @@ export type DnsConfigData = {
cache_ttl_max?: number;
cache_ttl_min?: number;
cache_optimistic?: boolean;
cache_enabled?: boolean;
};
export type FilteringData = {

View File

@@ -34,3 +34,7 @@
position: relative;
padding: 0 16px;
}
.inner {
padding-left: 32px;
}

View File

@@ -24,4 +24,8 @@
@media (min-width: 1024px) {
padding-top: 32px;
}
&_compact {
padding-bottom: 0;
}
}