mirror of
https://git.vectorsigma.ru/public/AdGuardHome.git
synced 2026-08-01 18:29:32 +00:00
ADG-10295 dns settings form
This commit is contained in:
@@ -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 don’t 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"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&.end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const Input = forwardRef<HTMLInputElement, Props>(
|
||||
disabled,
|
||||
error,
|
||||
errorMessage,
|
||||
size,
|
||||
size = 'large',
|
||||
autoComplete,
|
||||
},
|
||||
ref,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
29
client_v2/src/common/ui/Accordion/Accordion.module.pcss
Normal file
29
client_v2/src/common/ui/Accordion/Accordion.module.pcss
Normal 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);
|
||||
}
|
||||
37
client_v2/src/common/ui/Accordion/Accordion.tsx
Normal file
37
client_v2/src/common/ui/Accordion/Accordion.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
client_v2/src/common/ui/Accordion/index.ts
Normal file
1
client_v2/src/common/ui/Accordion/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Accordion } from './Accordion';
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--fills-backgrounds-page-background-additional);
|
||||
background-color: var(--page-background-additional);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
35
client_v2/src/components/DnsSettings/Access/Access.tsx
Normal file
35
client_v2/src/components/DnsSettings/Access/Access.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
147
client_v2/src/components/DnsSettings/Access/Form.tsx
Normal file
147
client_v2/src/components/DnsSettings/Access/Form.tsx
Normal 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 && <> ({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>
|
||||
);
|
||||
};
|
||||
1
client_v2/src/components/DnsSettings/Access/index.ts
Normal file
1
client_v2/src/components/DnsSettings/Access/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Access } from './Access';
|
||||
43
client_v2/src/components/DnsSettings/Cache/Cache.tsx
Normal file
43
client_v2/src/components/DnsSettings/Cache/Cache.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
194
client_v2/src/components/DnsSettings/Cache/Form.tsx
Normal file
194
client_v2/src/components/DnsSettings/Cache/Form.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
client_v2/src/components/DnsSettings/Cache/index.ts
Normal file
1
client_v2/src/components/DnsSettings/Cache/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Cache } from './Cache';
|
||||
50
client_v2/src/components/DnsSettings/DnsSettings.tsx
Normal file
50
client_v2/src/components/DnsSettings/DnsSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.subtitle {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.descriptions {
|
||||
padding: 12px 0 8px;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Form } from './Form';
|
||||
@@ -0,0 +1 @@
|
||||
export { ServerConfig } from './ServerConfig';
|
||||
89
client_v2/src/components/DnsSettings/Upstream/Upstream.tsx
Normal file
89
client_v2/src/components/DnsSettings/Upstream/Upstream.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { Examples } from './Examples';
|
||||
@@ -0,0 +1,8 @@
|
||||
.radioText {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-top: 8px;
|
||||
color: var(--default-attention-link);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Form } from './Form';
|
||||
1
client_v2/src/components/DnsSettings/Upstream/index.ts
Normal file
1
client_v2/src/components/DnsSettings/Upstream/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Upstream } from './Upstream';
|
||||
1
client_v2/src/components/DnsSettings/index.ts
Normal file
1
client_v2/src/components/DnsSettings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DnsSettings } from './DnsSettings';
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--fills-backgrounds-page-background-additional);
|
||||
background-color: var(--page-background-additional);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -321,6 +321,7 @@ export type DnsConfigData = {
|
||||
cache_ttl_max?: number;
|
||||
cache_ttl_min?: number;
|
||||
cache_optimistic?: boolean;
|
||||
cache_enabled?: boolean;
|
||||
};
|
||||
|
||||
export type FilteringData = {
|
||||
|
||||
@@ -34,3 +34,7 @@
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
@@ -24,4 +24,8 @@
|
||||
@media (min-width: 1024px) {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
&_compact {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user