mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
AGDNS-4173 update DNS settings page
This commit is contained in:
parent
4cbf10e68a
commit
4e3386a4a6
65 changed files with 2729 additions and 1826 deletions
|
|
@ -53,10 +53,10 @@
|
|||
"dns_server_configuration_saved_toast": "DNS server configuration saved",
|
||||
"dns_cache_configuration_saved_toast": "DNS cache configuration saved",
|
||||
"access_settings_saved_toast": "Access settings saved",
|
||||
"dns_test_ok_toast": "Specified DNS servers are working correctly",
|
||||
"dns_test_not_ok_toast": "Server \"%key%\": could not be used, please check that you've written it correctly",
|
||||
"dns_test_parsing_error_toast": "Section %section%: line %line%: could not be used, please check that you've written it correctly",
|
||||
"dns_test_warning_toast": "Upstream \"%key%\" does not respond to test requests and may not work properly",
|
||||
"dns_test_ok_toast": "All upstreams are operational",
|
||||
"dns_test_not_ok_toast": "Upstream %key% passed syntax check but failed the test. Verify the upstream settings",
|
||||
"dns_test_parsing_error_toast": "Failed to extract upstream name from line %number% in the %section% section. Check the line format",
|
||||
"dns_test_warning_toast": "Upstream %key% did not respond to the DNS query. Verify the upstream settings",
|
||||
"unblock": "Unblock",
|
||||
"block": "Block",
|
||||
"disallow_this_client": "Disallow this client",
|
||||
|
|
@ -276,19 +276,155 @@
|
|||
"encryption_confirm_clear_desc": "All encryption settings will be reset to their default values",
|
||||
"reset": "Reset",
|
||||
"confirm": "Confirm",
|
||||
"dns_settings": "DNS settings",
|
||||
"dns_access_settings_title": "Access settings",
|
||||
"dns_allowed_clients": "Allowed clients",
|
||||
"dns_allowed_clients_desc": "Only accepts requests from clients on this list",
|
||||
"dns_allowed_clients_desc_2": "To add a client, enter its <a>CIDR, IP address, or ClientID</a>",
|
||||
"dns_allowed_clients_label": "Allowed clients, one per line",
|
||||
"dns_allowed_clients_placeholder": "Enter allowed clients",
|
||||
"dns_blocking_mode": "Blocking mode",
|
||||
"dns_blocking_mode_custom_ip": "Custom IP address",
|
||||
"dns_blocking_mode_custom_ip_desc": "Responds with a manually set IP address",
|
||||
"dns_blocking_mode_default": "Default",
|
||||
"dns_blocking_mode_default_desc": "Responds with 0.0.0.0 for A records and :: for AAAA records if blocked by an Adblock-style rule. Responds with the IP address specified in the rule if blocked by a hosts-style rule",
|
||||
"dns_blocking_mode_desc": "Sets the response type to blocked requests",
|
||||
"dns_blocking_mode_ipv4_label": "Blocking IPv4",
|
||||
"dns_blocking_mode_ipv4_placeholder": "Enter IPv4 address",
|
||||
"dns_blocking_mode_ipv6_label": "Blocking IPv6",
|
||||
"dns_blocking_mode_ipv6_placeholder": "Enter IPv6 address",
|
||||
"dns_blocking_mode_null_ip": "Null IP address",
|
||||
"dns_blocking_mode_null_ip_desc": "Responds with 0.0.0.0 for A and :: for AAAA",
|
||||
"dns_blocking_mode_nxdomain": "NXDOMAIN",
|
||||
"dns_blocking_mode_nxdomain_desc": "Responds with NXDOMAIN code",
|
||||
"dns_blocking_mode_refused": "REFUSED",
|
||||
"dns_blocking_mode_refused_desc": "Responds with REFUSED code",
|
||||
"dns_blocking_mode_title": "Blocking mode",
|
||||
"dns_blocking_mode_ttl_label": "Blocked response TTL, in seconds",
|
||||
"dns_blocking_mode_ttl_placeholder": "Enter TTL",
|
||||
"dns_bootstrap_dns_desc": "Sets the DNS servers used to resolve DoH/DoT upstream resolver hostnames",
|
||||
"dns_bootstrap_dns_desc_2": "Enter IP addresses only. Comments are not allowed",
|
||||
"dns_bootstrap_dns_label": "Bootstrap DNS servers, one per line",
|
||||
"dns_bootstrap_dns_placeholder": "IP addresses",
|
||||
"dns_bootstrap_dns_title": "Bootstrap DNS servers",
|
||||
"dns_bootstrap_servers": "Bootstrap DNS servers",
|
||||
"dns_bootstrap_servers_desc": "Sets the DNS servers used to resolve DoH/DoT upstream resolver hostnames",
|
||||
"dns_cache_desc": "Stores DNS responses locally",
|
||||
"dns_cache_size": "Cache size",
|
||||
"dns_cache_size_desc": "Sets the DNS cache size",
|
||||
"dns_cache_size_label": "DNS cache size, in bytes",
|
||||
"dns_cache_size_title": "Cache size",
|
||||
"dns_cache_title": "DNS cache",
|
||||
"dns_clear_cache": "Clear cache",
|
||||
"dns_clear_cache_cancel": "Cancel",
|
||||
"dns_clear_cache_confirm": "Yes, clear",
|
||||
"dns_clear_cache_desc": "This will clear your device's DNS cache and prompt it to request new information from the DNS server",
|
||||
"dns_clear_cache_title": "Clear cache?",
|
||||
"dns_confirm_clear_cache": "Clear DNS cache?",
|
||||
"dns_confirm_clear_cache_desc": "Are you sure you want to clear the DNS cache?",
|
||||
"dns_clear_cache_yes": "Yes, clear",
|
||||
"dns_disallowed_clients": "Disallowed clients",
|
||||
"dns_disallowed_clients_desc": "Drops requests from clients on this list. This setting is ignored when there are entries in Allowed clients",
|
||||
"dns_disallowed_clients_label": "Disallowed clients, one per line",
|
||||
"dns_disallowed_clients_title": "Disallowed clients",
|
||||
"dns_disallowed_domains": "Disallowed domains",
|
||||
"dns_disallowed_domains_desc": "Drops DNS queries that match these domains, and they don't appear in Query log",
|
||||
"dns_disallowed_domains_desc_2": "Enter exact domain names, wildcards, or URL filter rules, such as example.org, *.example.org, or ||example.org^",
|
||||
"dns_disallowed_domains_label": "Disallowed domains, one per line",
|
||||
"dns_disallowed_domains_title": "Disallowed domains",
|
||||
"dns_dnssec": "DNSSEC",
|
||||
"dns_dnssec_desc": "Adds the DNSSEC flag in outbound DNS queries and checks the result. Requires a DNSSEC-enabled resolver",
|
||||
"dns_edns_client_subnet": "EDNS client subnet",
|
||||
"dns_edns_client_subnet_desc": "Adds EDNS Client Subnet (ECS) data to upstream requests and records ECS values in Query log",
|
||||
"dns_edns_custom_label": "Custom IP address for EDNS",
|
||||
"dns_edns_custom_placeholder": "Enter IP address",
|
||||
"dns_edns_desc": "Add EDNS Client Subnet (ECS) data to upstream requests and record ECS values in Query log. You can use the default client subnet or specify a custom IP address for ECS",
|
||||
"dns_edns_option_custom": "Use custom IP address",
|
||||
"dns_edns_option_default": "Use default EDNS client subnet",
|
||||
"dns_edns_title": "EDNS client subnet",
|
||||
"dns_fallback_dns_desc": "Sets fallback DNS servers to use when upstream DNS servers are unavailable",
|
||||
"dns_fallback_dns_desc_2": "Use the same syntax as for the <a>upstream DNS servers</a>",
|
||||
"dns_fallback_dns_label": "Fallback DNS servers, one per line",
|
||||
"dns_fallback_dns_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
|
||||
"dns_fallback_dns_title": "Fallback DNS servers",
|
||||
"dns_fallback_servers": "Fallback DNS servers",
|
||||
"dns_ipv6_resolution": "IPv6 resolution",
|
||||
"dns_ipv6_resolution_desc": "Enables resolution of DNS queries for IPv6 addresses (AAAA records) and adds IPv6 hints to HTTPS responses",
|
||||
"dns_optimistic_caching": "Optimistic caching",
|
||||
"dns_optimistic_caching_desc": "Responds from the cache even if the entries have expired and tries to update them",
|
||||
"dns_override_max_ttl": "Override maximum TTL",
|
||||
"dns_override_max_ttl_desc": "Sets the maximum time DNS responses are cached. Higher TTL values are reduced to this value",
|
||||
"dns_override_max_ttl_label": "Maximum time-to-live (TTL), in seconds",
|
||||
"dns_override_max_ttl_placeholder": "Time-to-live (TTL)",
|
||||
"dns_override_max_ttl_title": "Override maximum TTL",
|
||||
"dns_override_min_ttl": "Override minimum TTL",
|
||||
"dns_override_min_ttl_desc": "Sets the minimum time DNS responses are cached. Lower TTL values are increased to this value",
|
||||
"dns_override_min_ttl_label": "Minimum time-to-live (TTL), in seconds",
|
||||
"dns_override_min_ttl_placeholder": "Time-to-live (TTL)",
|
||||
"dns_override_min_ttl_title": "Override minimum TTL",
|
||||
"dns_private_reverse_page_desc": "Resolves PTR, SOA, and NS requests for ARPA domains with private IP addresses through private upstream servers, DHCP, and /etc/hosts. When disabled, responds to all such requests with NXDOMAIN",
|
||||
"dns_private_reverse_resolvers": "Private reverse DNS resolvers",
|
||||
"dns_private_reverse_resolvers_desc": "Resolves PTR, SOA, and NS requests for ARPA domains with private IP addresses through private upstream servers, DHCP, and /etc/hosts",
|
||||
"dns_private_reverse_resolvers_disabled_desc": "When disabled, responds to all such requests with NXDOMAIN",
|
||||
"dns_private_reverse_resolve_clients_desc": "Resolves client IP addresses to their hostnames using PTR queries. Uses private DNS servers for local clients and upstream servers for clients with public IP addresses",
|
||||
"dns_private_reverse_resolve_clients_title": "Reverse lookup of client IP addresses",
|
||||
"dns_private_reverse_servers_desc": "Sets the DNS servers for private PTR, SOA, and NS requests. These requests come from clients with private IP addresses and ask for ARPA domains containing subnets from private IP address ranges",
|
||||
"dns_private_reverse_servers_desc_2": "If this field is empty, your operating system's default DNS resolvers are used, except for AdGuard Home IP addresses. By default, AdGuard Home uses the following reverse DNS resolvers: %value_1% and %value_2%",
|
||||
"dns_private_reverse_servers_label": "Private reverse DNS servers, one per line",
|
||||
"dns_private_reverse_servers_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
|
||||
"dns_private_reverse_servers_title": "Private reverse DNS servers",
|
||||
"dns_rate_limit": "Rate limit",
|
||||
"dns_rate_limit_allowlist": "Rate-limiting allowlist",
|
||||
"dns_rate_limit_allowlist_desc": "Specifies IP addresses excluded from rate limiting",
|
||||
"dns_rate_limit_allowlist_label": "IP addresses, one per line",
|
||||
"dns_rate_limit_allowlist_placeholder": "IP addresses",
|
||||
"dns_rate_limit_allowlist_title": "Rate-limiting allowlist",
|
||||
"dns_rate_limit_desc": "Sets the number of requests per second allowed per client",
|
||||
"dns_rate_limit_desc_2": "A value of 0 means no limit",
|
||||
"dns_rate_limit_no_limit": "No limit",
|
||||
"dns_rate_limit_placeholder": "Enter rate limit",
|
||||
"dns_rate_limit_title": "Rate limit",
|
||||
"dns_rate_limit_value": "%value% req/s",
|
||||
"dns_server_addresses": "Server addresses",
|
||||
"dns_server_addresses_configured_in_file": "Server addresses are configured in the file %path%",
|
||||
"dns_server_addresses_desc": "Sets DNS server addresses through which AdGuard Home will be accessible",
|
||||
"dns_server_addresses_desc_2": "View tips for <a>configuring upstream DNS servers</a> and our <b>list of trusted DNS providers</b>",
|
||||
"dns_server_addresses_label": "Server addresses, one per line",
|
||||
"dns_server_addresses_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
|
||||
"dns_server_addresses_title": "Server addresses",
|
||||
"dns_server_config_title": "DNS server configuration",
|
||||
"dns_subnet_prefix": "Subnet prefix length for %value% addresses",
|
||||
"dns_subnet_prefix_desc": "Sets the subnet prefix length for %value% addresses used for rate limiting",
|
||||
"dns_subnet_prefix_title": "Subnet prefix length for %value% addresses",
|
||||
"dns_subnet_placeholder": "Enter prefix length",
|
||||
"dns_test_upstreams": "Test upstreams",
|
||||
"dns_test_upstreams_error_format": "Failed to extract upstream name from line %number% in the %section% section. Check the line format",
|
||||
"dns_test_upstreams_success": "All upstreams are operational",
|
||||
"dns_test_upstreams_warning_query": "Upstream %name% did not respond to the DNS query. Verify the upstream settings",
|
||||
"dns_test_upstreams_warning_syntax": "Upstream %name% passed syntax check but failed the test. Verify the upstream settings",
|
||||
"dns_upstream_mode": "Upstream mode",
|
||||
"dns_upstream_mode_desc": "Determines how to route your network's DNS queries to multiple upstream servers",
|
||||
"dns_upstream_mode_title": "Upstream mode",
|
||||
"dns_upstream_servers_title": "Upstream DNS servers",
|
||||
"dns_upstream_timeout": "Upstream timeout",
|
||||
"dns_upstream_timeout_desc": "Sets how long to wait for a response from upstream servers",
|
||||
"dns_upstream_timeout_placeholder": "Enter timeout in seconds",
|
||||
"dns_upstream_timeout_title": "Upstream timeout",
|
||||
"dns_ttl_value": "%value% s",
|
||||
"dns_upstream_validation_invalid": "Invalid format",
|
||||
"dns_upstream_validation_invalid_lines": "Invalid format on lines {lines}",
|
||||
"dns_settings": "DNS",
|
||||
"upstream_dns": "Upstream DNS servers",
|
||||
"upstream_dns_servers_title": "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": "<a>Learn more</a> about configuring upstream DNS servers. Here is a <b>list of known DNS providers</b> to choose from.",
|
||||
"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_load_balancing": "Load balancing",
|
||||
"upstream_dns_load_balancing_desc": "<p>Query one upstream server at a time</p><p>AdGuard Home uses a weighted random algorithm to select servers with the lowest number of failed lookups and the lowest average lookup time</p>",
|
||||
"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_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_dns_fastest_addr_desc": "Wait for responses from all 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)",
|
||||
|
|
|
|||
|
|
@ -2,15 +2,26 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getDnsConfig: vi.fn(),
|
||||
setDnsConfig: vi.fn(),
|
||||
addErrorToast: vi.fn(),
|
||||
addSuccessToast: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('panel/api/Api', () => ({
|
||||
apiClient: { getDnsConfig: mocks.getDnsConfig },
|
||||
apiClient: {
|
||||
getDnsConfig: mocks.getDnsConfig,
|
||||
setDnsConfig: mocks.setDnsConfig,
|
||||
},
|
||||
}));
|
||||
vi.mock('panel/stores/toasts', () => ({
|
||||
addErrorToast: mocks.addErrorToast,
|
||||
addSuccessToast: mocks.addSuccessToast,
|
||||
}));
|
||||
vi.mock('panel/common/intl', () => ({
|
||||
default: { getMessage: vi.fn((key: string) => key) },
|
||||
}));
|
||||
vi.mock('panel/stores/toasts', () => ({ addErrorToast: mocks.addErrorToast }));
|
||||
|
||||
import { getDnsConfig, dnsConfigState } from 'panel/stores/dnsConfig';
|
||||
import { getDnsConfig, dnsConfigState, toggleResolveClients } from 'panel/stores/dnsConfig';
|
||||
|
||||
describe('getDnsConfig', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
|
@ -27,3 +38,29 @@ describe('getDnsConfig', () => {
|
|||
expect(dnsConfigState.upstream_mode).toBe('load_balance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleResolveClients', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('toggles resolve_clients and persists (FR-032)', async () => {
|
||||
mocks.setDnsConfig.mockResolvedValue({});
|
||||
|
||||
const before = dnsConfigState.resolve_clients;
|
||||
await toggleResolveClients();
|
||||
expect(dnsConfigState.resolve_clients).toBe(!before);
|
||||
|
||||
// Toggle back
|
||||
await toggleResolveClients();
|
||||
expect(dnsConfigState.resolve_clients).toBe(before);
|
||||
});
|
||||
|
||||
it('calls apiClient.setDnsConfig with inverted value (FR-032)', async () => {
|
||||
mocks.setDnsConfig.mockResolvedValue({});
|
||||
|
||||
const before = dnsConfigState.resolve_clients;
|
||||
await toggleResolveClients();
|
||||
expect(mocks.setDnsConfig).toHaveBeenCalledWith({
|
||||
resolve_clients: !before,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
37
client_v3/src/__tests__/helpers/constants.test.ts
Normal file
37
client_v3/src/__tests__/helpers/constants.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('panel/common/intl', () => ({
|
||||
default: { getMessage: vi.fn((key: string) => key) },
|
||||
}));
|
||||
|
||||
import { getUpstreamModeOptions, DNS_REQUEST_OPTIONS } from 'panel/helpers/constants';
|
||||
|
||||
describe('getUpstreamModeOptions', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('returns exactly 3 options (FR-003)', () => {
|
||||
const options = getUpstreamModeOptions();
|
||||
expect(options).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('returns LOAD_BALANCING, PARALLEL, FASTEST_ADDR values (FR-003)', () => {
|
||||
const options = getUpstreamModeOptions();
|
||||
expect(options[0].value).toBe(DNS_REQUEST_OPTIONS.LOAD_BALANCING);
|
||||
expect(options[1].value).toBe(DNS_REQUEST_OPTIONS.PARALLEL);
|
||||
expect(options[2].value).toBe(DNS_REQUEST_OPTIONS.FASTEST_ADDR);
|
||||
});
|
||||
|
||||
it('returns i18n-correct text (factory re-executes getMessage)', () => {
|
||||
const options = getUpstreamModeOptions();
|
||||
expect(options[0].text).toBe('upstream_dns_load_balancing');
|
||||
expect(options[1].text).toBe('upstream_dns_parallel_requests');
|
||||
expect(options[2].text).toBe('upstream_dns_fastest_addr');
|
||||
});
|
||||
|
||||
it('returns warning only for fastest_addr option', () => {
|
||||
const options = getUpstreamModeOptions();
|
||||
expect(options[0].warning).toBeUndefined();
|
||||
expect(options[1].warning).toBeUndefined();
|
||||
expect(options[2].warning).toBe('upstream_dns_fastest_addr_warning');
|
||||
});
|
||||
});
|
||||
|
|
@ -22,6 +22,33 @@
|
|||
&.end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&.modal {
|
||||
align-items: flex-start;
|
||||
|
||||
.text {
|
||||
max-width: 100%;
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--fs-text-t3);
|
||||
line-height: var(--lh-t3-normal);
|
||||
font-weight: var(--weight-regular);
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
|
|
@ -31,6 +58,10 @@
|
|||
.text {
|
||||
max-width: 250px;
|
||||
flex: 1;
|
||||
|
||||
&.fullWidth {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type Props<T = string | number | boolean> = {
|
|||
textClass?: string;
|
||||
verticalAlign?: 'center' | 'start' | 'end';
|
||||
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
||||
inModal?: boolean;
|
||||
};
|
||||
|
||||
export const Radio = <T extends number | string | boolean = string>(props: Props<T>) => {
|
||||
|
|
@ -35,6 +36,9 @@ export const Radio = <T extends number | string | boolean = string>(props: Props
|
|||
s.radio,
|
||||
props.class,
|
||||
props.verticalAlign && s[props.verticalAlign],
|
||||
{
|
||||
[s.modal]: props.inModal,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<input
|
||||
|
|
@ -52,7 +56,11 @@ export const Radio = <T extends number | string | boolean = string>(props: Props
|
|||
class={cn(s.icon, { [s.active]: props.value === o.value })}
|
||||
/>
|
||||
</div>
|
||||
<div class={cn(s.text, props.textClass)}>
|
||||
<div
|
||||
class={cn(s.text, {
|
||||
[props.textClass!]: props.textClass,
|
||||
})}
|
||||
>
|
||||
<div>{o.text}</div>
|
||||
<Show when={o.description}>
|
||||
<div class={cn(theme.text.t4, s.description)}>{o.description}</div>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export const Textarea = (props: Props) => {
|
|||
cols={props.cols}
|
||||
rows={props.rows}
|
||||
onChange={(e) => (props.onChange as any)?.(e)}
|
||||
onBlur={(e) => (props.onBlur as any)?.(e)}
|
||||
wrap={props.wrap}
|
||||
maxLength={props.maxLength}
|
||||
disabled={props.disabled}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
text-align: left;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
|
||||
&.compact {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type Props = {
|
|||
children: JSX.Element;
|
||||
defaultOpen?: boolean;
|
||||
class?: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export const Accordion = (props: Props) => {
|
||||
|
|
@ -23,7 +24,7 @@ export const Accordion = (props: Props) => {
|
|||
<div class={cn(s.accordion, props.class)}>
|
||||
<button
|
||||
type="button"
|
||||
class={s.header}
|
||||
class={cn(s.header, { [s.compact]: props.compact })}
|
||||
onClick={toggleOpen}
|
||||
aria-expanded={isOpen()}
|
||||
aria-controls="accordion-content"
|
||||
|
|
|
|||
|
|
@ -29,5 +29,8 @@
|
|||
|
||||
.description {
|
||||
padding: 8px 16px;
|
||||
color: var(--default-description-text);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type JSX } from 'solid-js';
|
||||
import { type JSX, Show } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { Dialog } from 'panel/common/ui/Dialog';
|
||||
|
|
@ -17,7 +17,8 @@ type Props = {
|
|||
class?: string;
|
||||
children?: JSX.Element;
|
||||
footer?: JSX.Element;
|
||||
description?: string;
|
||||
description?: string | JSX.Element;
|
||||
hideSubmit?: boolean;
|
||||
};
|
||||
|
||||
export const ConfigDialog = (props: Props) => {
|
||||
|
|
@ -37,15 +38,17 @@ export const ConfigDialog = (props: Props) => {
|
|||
</fieldset>
|
||||
<div class={s.footer}>
|
||||
{props.footer}
|
||||
<Button
|
||||
variant="primary"
|
||||
class={s.saveButton}
|
||||
disabled={isDisabled()}
|
||||
data-testid="config-dialog-save"
|
||||
onClick={props.onSubmit}
|
||||
>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
<Show when={!props.hideSubmit}>
|
||||
<Button
|
||||
variant="primary"
|
||||
class={s.saveButton}
|
||||
disabled={isDisabled()}
|
||||
data-testid="config-dialog-save"
|
||||
onClick={props.onSubmit}
|
||||
>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ export const Dropdown = (props: Props) => {
|
|||
// to plain `bottom`/`top` (ignoring left/right), so mapping `bottomRight` ->
|
||||
// `bottom-end` is an intentional behavior change that honors the `position`
|
||||
// prop. `flip` keeps the popover on-screen near viewport edges.
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const placement = () => {
|
||||
switch (props.position) {
|
||||
const position = untrack(() => props.position);
|
||||
switch (position) {
|
||||
case 'topRight':
|
||||
return 'top-end';
|
||||
case 'topLeft':
|
||||
|
|
|
|||
|
|
@ -39,10 +39,8 @@ export const LanguageDropdown = (props: LanguageDropdownProps) => {
|
|||
if (!props.sort) {
|
||||
return keys;
|
||||
}
|
||||
// eslint-disable-next-line solid/reactivity -- sort callback inside createMemo; reads already tracked
|
||||
return keys.sort((a, b) =>
|
||||
(props.languages[a] || '').localeCompare(props.languages[b] || ''),
|
||||
);
|
||||
const languages = props.languages;
|
||||
return keys.sort((a, b) => (languages[a] || '').localeCompare(languages[b] || ''));
|
||||
});
|
||||
|
||||
const currentLabel = () => getLanguageShortLabel(props.value);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@
|
|||
|
||||
.desc {
|
||||
color: var(--default-description-text);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.descDisabled {
|
||||
|
|
@ -93,6 +97,10 @@
|
|||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
align-self: stretch;
|
||||
|
||||
&.largeTitle {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ type SettingRowVariant = 'switch' | 'link' | 'switch-link';
|
|||
type Props = {
|
||||
id: string;
|
||||
title: string;
|
||||
titleClass?: string;
|
||||
description?: string | JSX.Element;
|
||||
descriptionClass?: string;
|
||||
value?: string;
|
||||
variant: SettingRowVariant;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
titleClass?: string;
|
||||
onChange?: (checked: boolean) => void;
|
||||
onClick?: () => void;
|
||||
class?: string;
|
||||
children?: JSX.Element;
|
||||
divider?: boolean;
|
||||
align?: 'top' | 'center';
|
||||
largeTitle?: boolean;
|
||||
};
|
||||
|
||||
export const SettingRow = (props: Props) => {
|
||||
|
|
@ -119,7 +121,9 @@ export const SettingRow = (props: Props) => {
|
|||
</div>
|
||||
<Show when={props.description}>
|
||||
<div
|
||||
class={cn(s.desc, theme.text.t3, { [s.descDisabled]: props.disabled })}
|
||||
class={cn(s.desc, props.descriptionClass, theme.text.t3, {
|
||||
[s.descDisabled]: props.disabled,
|
||||
})}
|
||||
>
|
||||
{props.description}
|
||||
</div>
|
||||
|
|
@ -137,7 +141,10 @@ export const SettingRow = (props: Props) => {
|
|||
<Show when={isSwitchLink() && props.divider}>
|
||||
<div class={s.divider} />
|
||||
</Show>
|
||||
<div class={s.input} onClick={handleInputClick}>
|
||||
<div
|
||||
class={cn(s.input, props.largeTitle && s.largeTitle)}
|
||||
onClick={handleInputClick}
|
||||
>
|
||||
<Show when={isSwitchVariant()}>
|
||||
<Switch
|
||||
id={props.id}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type JSX, createMemo, For, Show } from 'solid-js';
|
||||
import { type JSX, createMemo, For, Show, untrack } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import cn from 'clsx';
|
||||
|
||||
|
|
@ -56,8 +56,7 @@ export interface TableProps<T = any> {
|
|||
export const Table = <T extends Record<string, any>>(props: TableProps<T>) => {
|
||||
const [state, setState] = createStore({
|
||||
currentPage: 0,
|
||||
// eslint-disable-next-line solid/reactivity -- one-time initial value from props
|
||||
pageSize: props.pageSize ?? DEFAULT_PAGE_SIZE_OPTIONS[0],
|
||||
pageSize: untrack(() => props.pageSize) ?? DEFAULT_PAGE_SIZE_OPTIONS[0],
|
||||
sortKey: props.defaultSort?.key ?? (null as string | null),
|
||||
sortDirection: props.defaultSort?.direction ?? ('asc' as 'asc' | 'desc'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { getDnsStatus, getTimerStatus, dashboardState } from '../../stores/dashb
|
|||
|
||||
import s from './styles.module.pcss';
|
||||
import { DnsSettings } from '../DnsSettings';
|
||||
import { PrivateReverse } from '../DnsSettings/PrivateReverse';
|
||||
import { UserRules } from '../UserRules';
|
||||
import { BlockedServices } from '../BlockedServices';
|
||||
import { Clients } from '../Clients/Clients';
|
||||
|
|
@ -126,6 +127,7 @@ const App = () => {
|
|||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/encryption" component={Encryption} />
|
||||
<Route path="/dns" component={DnsSettings} />
|
||||
<Route path="/dns/private-reverse" component={PrivateReverse} />
|
||||
<Route path="/blocklists" component={Blocklists} />
|
||||
<Route path="/allowlists" component={Allowlists} />
|
||||
<Route path="/user_rules" component={UserRules} />
|
||||
|
|
|
|||
|
|
@ -14,12 +14,7 @@ import { RoutePath } from 'panel/components/Routes/Paths';
|
|||
import { buildClientBreadcrumbs } from 'panel/helpers/buildClientBreadcrumbs';
|
||||
import { ScheduleRow } from './ScheduleRow';
|
||||
import { ScheduleModal } from './ScheduleModal';
|
||||
import {
|
||||
type DayKey,
|
||||
DAYS_OF_WEEK,
|
||||
type ScheduleData,
|
||||
type ScheduleDayData,
|
||||
} from './helpers';
|
||||
import { type DayKey, DAYS_OF_WEEK, type ScheduleData, type ScheduleDayData } from './helpers';
|
||||
import { getDayName } from './getDayName';
|
||||
import s from './InactivitySchedule.module.pcss';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createMemo, Show } from 'solid-js';
|
||||
import { createMemo, Show, untrack } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
import {
|
||||
Chart,
|
||||
|
|
@ -109,8 +109,7 @@ export const StatCard = (props: StatCardProps) => {
|
|||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- color is stable per card; plugin captures at mount
|
||||
const cursorLinePlugin = createCursorLinePlugin(props.color);
|
||||
const cursorLinePlugin = createCursorLinePlugin(untrack(() => props.color));
|
||||
|
||||
const externalTooltipHandler = createExternalTooltipHandler(
|
||||
() => tooltipEl,
|
||||
|
|
@ -155,8 +154,7 @@ export const StatCard = (props: StatCardProps) => {
|
|||
|
||||
const percent = () => props.percentValue ?? 0;
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- accessors consumed inside useChart's createEffect/onMount
|
||||
const setCanvasRef = useChart(chartData, chartOptions, [cursorLinePlugin]);
|
||||
const setCanvasRef = untrack(() => useChart(chartData, chartOptions, [cursorLinePlugin]));
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -27,21 +27,19 @@ export const useSortedData = <T extends SortableItem>(
|
|||
const [sortDirection, setSortDirection] = createSignal<SortDirection>(defaultSortDirection);
|
||||
|
||||
const sortedData = createMemo(() => {
|
||||
return (
|
||||
[...data()]
|
||||
// eslint-disable-next-line solid/reactivity -- sort callback inside createMemo; reads already tracked
|
||||
.sort((a, b) => {
|
||||
const modifier = sortDirection() === 'asc' ? 1 : -1;
|
||||
if (sortField() === 'name') {
|
||||
return a.name.localeCompare(b.name) * modifier;
|
||||
}
|
||||
return (a.count - b.count) * modifier;
|
||||
})
|
||||
.slice(0, limit)
|
||||
);
|
||||
const direction = sortDirection();
|
||||
const field = sortField();
|
||||
return [...data()]
|
||||
.sort((a, b) => {
|
||||
const modifier = direction === 'asc' ? 1 : -1;
|
||||
if (field === 'name') {
|
||||
return a.name.localeCompare(b.name) * modifier;
|
||||
}
|
||||
return (a.count - b.count) * modifier;
|
||||
})
|
||||
.slice(0, limit);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- returned to consumers, called from JSX (tracked)
|
||||
const handleSort = (field: SortField) => {
|
||||
untrack(() => {
|
||||
if (sortField() === field) {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,79 @@
|
|||
import { createMemo } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { accessState, setAccessList } from 'panel/stores/access';
|
||||
import { accessState } from 'panel/stores/access';
|
||||
import intl from 'panel/common/intl';
|
||||
import { SettingRow } from 'panel/common/ui/SettingRow';
|
||||
import { useDialogOpen } from 'panel/hooks/useField';
|
||||
import { getListSummary } from '../helpers';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import { Form } from './Form';
|
||||
import { AllowedClientsDialog } from './blocks/AllowedClientsDialog';
|
||||
import { DisallowedClientsDialog } from './blocks/DisallowedClientsDialog';
|
||||
import { DisallowedDomainsDialog } from './blocks/DisallowedDomainsDialog';
|
||||
|
||||
export const Access = () => {
|
||||
const handleFormSubmit = (values: any) => {
|
||||
setAccessList(values);
|
||||
};
|
||||
const allowedDialog = useDialogOpen();
|
||||
const disallowedClientsDialog = useDialogOpen();
|
||||
const disallowedDomainsDialog = useDialogOpen();
|
||||
|
||||
const allowedClientsOn = createMemo(() => accessState.allowed_clients.trim().length > 0);
|
||||
const processing = () => accessState.processingSet;
|
||||
|
||||
const allowedClientsValue = createMemo(() => getListSummary(accessState.allowed_clients));
|
||||
const disallowedClientsValue = createMemo(() => getListSummary(accessState.disallowed_clients));
|
||||
const disallowedDomainsValue = createMemo(() => getListSummary(accessState.blocked_hosts));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2
|
||||
class={cn(
|
||||
theme.layout.subtitle,
|
||||
theme.layout.subtitle_compact,
|
||||
theme.title.h5,
|
||||
theme.title.h4_tablet,
|
||||
)}
|
||||
>
|
||||
{intl.getMessage('access_settings_title')}
|
||||
<h2 class={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('dns_access_settings_title')}
|
||||
</h2>
|
||||
|
||||
<Form
|
||||
initialValues={accessState}
|
||||
onSubmit={handleFormSubmit}
|
||||
processingSet={accessState.processingSet}
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="allowed_clients"
|
||||
title={intl.getMessage('dns_allowed_clients')}
|
||||
description={intl.getMessage('dns_allowed_clients_desc')}
|
||||
value={allowedClientsValue()}
|
||||
onClick={allowedDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="disallowed_clients"
|
||||
title={intl.getMessage('dns_disallowed_clients')}
|
||||
description={intl.getMessage('dns_disallowed_clients_desc')}
|
||||
value={disallowedClientsValue()}
|
||||
disabled={allowedClientsOn()}
|
||||
onClick={disallowedClientsDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="disallowed_domains"
|
||||
title={intl.getMessage('dns_disallowed_domains')}
|
||||
description={intl.getMessage('dns_disallowed_domains_desc')}
|
||||
value={disallowedDomainsValue()}
|
||||
onClick={disallowedDomainsDialog.openDialog}
|
||||
/>
|
||||
|
||||
<AllowedClientsDialog
|
||||
open={allowedDialog.open}
|
||||
onClose={allowedDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<DisallowedClientsDialog
|
||||
open={disallowedClientsDialog.open}
|
||||
onClose={disallowedClientsDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<DisallowedDomainsDialog
|
||||
open={disallowedDomainsDialog.open}
|
||||
onClose={disallowedDomainsDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,221 +0,0 @@
|
|||
import { createSignal, createMemo, For } from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
|
||||
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 { validateIpPerLine } from 'panel/helpers/validators';
|
||||
import { removeEmptyLines, trimMultilineString } from 'panel/helpers/helpers';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type FormData = {
|
||||
allowed_clients: string;
|
||||
disallowed_clients: string;
|
||||
blocked_hosts: string;
|
||||
};
|
||||
|
||||
type FormProps = {
|
||||
initialValues?: {
|
||||
allowed_clients?: string;
|
||||
disallowed_clients?: string;
|
||||
blocked_hosts?: string;
|
||||
};
|
||||
onSubmit: (data: FormData) => void;
|
||||
processingSet: boolean;
|
||||
};
|
||||
|
||||
export const Form = (props: FormProps) => {
|
||||
const [allowedClients, setAllowedClients] = createSignal(
|
||||
props.initialValues?.allowed_clients || '',
|
||||
);
|
||||
const [disallowedClients, setDisallowedClients] = createSignal(
|
||||
props.initialValues?.disallowed_clients || '',
|
||||
);
|
||||
const [blockedHosts, setBlockedHosts] = createSignal(props.initialValues?.blocked_hosts || '');
|
||||
|
||||
const [allowedClientsError, setAllowedClientsError] = createSignal('');
|
||||
const [disallowedClientsError, setDisallowedClientsError] = createSignal('');
|
||||
|
||||
const getFieldValue = (id: keyof FormData) => {
|
||||
switch (id) {
|
||||
case 'allowed_clients':
|
||||
return allowedClients();
|
||||
case 'disallowed_clients':
|
||||
return disallowedClients();
|
||||
case 'blocked_hosts':
|
||||
return blockedHosts();
|
||||
}
|
||||
};
|
||||
|
||||
const setFieldValue = (id: keyof FormData, value: string) => {
|
||||
switch (id) {
|
||||
case 'allowed_clients':
|
||||
setAllowedClients(value);
|
||||
break;
|
||||
case 'disallowed_clients':
|
||||
setDisallowedClients(value);
|
||||
break;
|
||||
case 'blocked_hosts':
|
||||
setBlockedHosts(value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldError = (id: keyof FormData) => {
|
||||
switch (id) {
|
||||
case 'allowed_clients':
|
||||
return allowedClientsError();
|
||||
case 'disallowed_clients':
|
||||
return disallowedClientsError();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const validateField = (id: keyof FormData) => {
|
||||
const isIpField = id === 'allowed_clients' || id === 'disallowed_clients';
|
||||
if (!isIpField) return;
|
||||
const value = getFieldValue(id);
|
||||
const err = value ? validateIpPerLine(value) : undefined;
|
||||
if (id === 'allowed_clients') setAllowedClientsError(err || '');
|
||||
if (id === 'disallowed_clients') setDisallowedClientsError(err || '');
|
||||
};
|
||||
|
||||
const handleDisabledFieldState = (id: string) => {
|
||||
return id === 'disallowed_clients' && !!allowedClients();
|
||||
};
|
||||
|
||||
const getPlaceholder = (id: string) => {
|
||||
if (id === 'allowed_clients') {
|
||||
return intl.getMessage('access_settings_allowed_placeholder');
|
||||
}
|
||||
if (id === 'disallowed_clients') {
|
||||
return intl.getMessage('access_settings_disallowed_placeholder');
|
||||
}
|
||||
if (id === 'blocked_hosts') {
|
||||
return intl.getMessage('access_settings_blocked_placeholder');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
validateField('allowed_clients');
|
||||
validateField('disallowed_clients');
|
||||
|
||||
if (allowedClientsError() || disallowedClientsError()) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onSubmit({
|
||||
allowed_clients: allowedClients(),
|
||||
disallowed_clients: disallowedClients(),
|
||||
blocked_hosts: blockedHosts(),
|
||||
});
|
||||
};
|
||||
|
||||
const fields = createMemo<
|
||||
{
|
||||
id: keyof FormData;
|
||||
title: string;
|
||||
faq: JSX.Element;
|
||||
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,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class={theme.form.form}>
|
||||
<div class={theme.form.group}>
|
||||
<For each={fields()}>
|
||||
{(f) => (
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={getFieldValue(f.id)}
|
||||
onChange={(e: Event) =>
|
||||
setFieldValue(f.id, (e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={() => {
|
||||
const field = fields().find((ff) => ff.id === f.id);
|
||||
if (field) {
|
||||
setFieldValue(
|
||||
f.id,
|
||||
field.normalizeOnBlur(getFieldValue(f.id)),
|
||||
);
|
||||
}
|
||||
validateField(f.id);
|
||||
}}
|
||||
id={f.id}
|
||||
data-testid={f.id}
|
||||
label={
|
||||
<>
|
||||
{f.title}
|
||||
<FaqTooltip
|
||||
text={f.faq}
|
||||
menuSize="large"
|
||||
spacing={f.id === 'blocked_hosts'}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
errorMessage={getFieldError(f.id)}
|
||||
size="medium"
|
||||
disabled={handleDisabledFieldState(f.id)}
|
||||
placeholder={getPlaceholder(f.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.buttonGroup}>
|
||||
<Button
|
||||
type="submit"
|
||||
id="access_save"
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={props.processingSet}
|
||||
class={theme.form.button}
|
||||
>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { accessState, setAccessList } from 'panel/stores/access';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateIpPerLine } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const AllowedClientsDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => accessState.allowed_clients,
|
||||
{ validate: (v) => (v ? validateIpPerLine(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_allowed_clients')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_allowed_clients_desc')}</p>
|
||||
<p>
|
||||
{intl.getMessage('dns_allowed_clients_desc_2', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#identifying-clients"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={theme.link.link}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setAccessList({ allowed_clients: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id="allowed_clients"
|
||||
label={intl.getMessage('dns_allowed_clients_label')}
|
||||
placeholder={intl.getMessage('dns_allowed_clients_placeholder')}
|
||||
size="medium"
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { accessState, setAccessList } from 'panel/stores/access';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateIpPerLine } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const DisallowedClientsDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => accessState.disallowed_clients,
|
||||
{ validate: (v) => (v ? validateIpPerLine(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_disallowed_clients')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_disallowed_clients_desc')}</p>
|
||||
<p>
|
||||
{intl.getMessage('dns_allowed_clients_desc_2', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#identifying-clients"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={theme.link.link}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setAccessList({ disallowed_clients: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id="disallowed_clients"
|
||||
label={intl.getMessage('dns_disallowed_clients_label')}
|
||||
size="medium"
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { type Accessor } from 'solid-js';
|
||||
|
||||
import { accessState, setAccessList } from 'panel/stores/access';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const DisallowedDomainsDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => accessState.blocked_hosts,
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_disallowed_domains')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_disallowed_domains_desc')}</p>
|
||||
<p>{intl.getMessage('dns_disallowed_domains_desc_2')}</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
setAccessList({ blocked_hosts: field.value() });
|
||||
props.onClose();
|
||||
}}
|
||||
processing={props.processing}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
id="blocked_hosts"
|
||||
label={intl.getMessage('dns_disallowed_domains_label')}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
13
client_v3/src/components/DnsSettings/Cache/Cache.module.pcss
Normal file
13
client_v3/src/components/DnsSettings/Cache/Cache.module.pcss
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.section {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--fs-t2-normal);
|
||||
line-height: var(--lh-t2-normal);
|
||||
}
|
||||
|
|
@ -1,33 +1,135 @@
|
|||
import { createSignal, createMemo, Show } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import { replaceEmptyStringsWithZeroes, replaceZeroWithEmptyString } from 'panel/helpers/helpers';
|
||||
import {
|
||||
dnsConfigState,
|
||||
clearDnsCache,
|
||||
toggleCacheEnabled,
|
||||
toggleOptimisticCaching,
|
||||
} from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import { SettingRow } from 'panel/common/ui/SettingRow';
|
||||
import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import { useDialogOpen } from 'panel/hooks/useField';
|
||||
import { getCacheSizeSummary, getTtlSummary } from '../helpers';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import { Form } from './Form';
|
||||
import s from './Cache.module.pcss';
|
||||
import { CacheInputDialog } from './blocks/CacheInputDialog';
|
||||
|
||||
export const Cache = () => {
|
||||
const handleFormSubmit = (values: any) => {
|
||||
const completedFields = replaceEmptyStringsWithZeroes(values);
|
||||
setDnsConfig(completedFields, intl.getMessage('dns_cache_configuration_saved_toast'));
|
||||
};
|
||||
const [showClearConfirm, setShowClearConfirm] = createSignal(false);
|
||||
|
||||
const cacheSizeDialog = useDialogOpen();
|
||||
const minTtlDialog = useDialogOpen();
|
||||
const maxTtlDialog = useDialogOpen();
|
||||
|
||||
const cacheSizeValue = createMemo(() => getCacheSizeSummary(dnsConfigState.cache_size));
|
||||
const minTtlValue = createMemo(() => getTtlSummary(dnsConfigState.cache_ttl_min));
|
||||
const maxTtlValue = createMemo(() => getTtlSummary(dnsConfigState.cache_ttl_max));
|
||||
|
||||
const processing = () => dnsConfigState.processingSetConfig;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 class={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('dns_cache_config')}
|
||||
</h2>
|
||||
<div class={s.section}>
|
||||
<SettingRow
|
||||
variant="switch"
|
||||
id="cache_enabled"
|
||||
title={intl.getMessage('dns_cache_title')}
|
||||
titleClass={cn(theme.title.h5, theme.title.h4_tablet, theme.text.bold, s.title)}
|
||||
descriptionClass={s.description}
|
||||
align="center"
|
||||
description={intl.getMessage('dns_cache_desc')}
|
||||
checked={!!dnsConfigState.cache_enabled}
|
||||
onChange={() => toggleCacheEnabled()}
|
||||
/>
|
||||
|
||||
<Form
|
||||
initialValues={{
|
||||
cache_enabled: dnsConfigState.cache_enabled,
|
||||
cache_size: replaceZeroWithEmptyString(dnsConfigState.cache_size),
|
||||
cache_ttl_max: replaceZeroWithEmptyString(dnsConfigState.cache_ttl_max),
|
||||
cache_ttl_min: replaceZeroWithEmptyString(dnsConfigState.cache_ttl_min),
|
||||
cache_optimistic: dnsConfigState.cache_optimistic,
|
||||
}}
|
||||
onSubmit={handleFormSubmit}
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="cache_size"
|
||||
title={intl.getMessage('dns_cache_size')}
|
||||
description={intl.getMessage('dns_cache_size_desc')}
|
||||
value={cacheSizeValue()}
|
||||
disabled={!dnsConfigState.cache_enabled}
|
||||
onClick={cacheSizeDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="override_min_ttl"
|
||||
title={intl.getMessage('dns_override_min_ttl')}
|
||||
description={intl.getMessage('dns_override_min_ttl_desc')}
|
||||
value={minTtlValue()}
|
||||
disabled={!dnsConfigState.cache_enabled}
|
||||
onClick={minTtlDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="override_max_ttl"
|
||||
title={intl.getMessage('dns_override_max_ttl')}
|
||||
description={intl.getMessage('dns_override_max_ttl_desc')}
|
||||
value={maxTtlValue()}
|
||||
disabled={!dnsConfigState.cache_enabled}
|
||||
onClick={maxTtlDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="switch"
|
||||
id="optimistic_caching"
|
||||
title={intl.getMessage('dns_optimistic_caching')}
|
||||
description={intl.getMessage('dns_optimistic_caching_desc')}
|
||||
checked={!!dnsConfigState.cache_optimistic}
|
||||
disabled={!dnsConfigState.cache_enabled}
|
||||
onChange={() => toggleOptimisticCaching()}
|
||||
/>
|
||||
|
||||
<div class={theme.form.actionRow}>
|
||||
<Button
|
||||
variant="secondary-danger"
|
||||
onClick={() => setShowClearConfirm(true)}
|
||||
class={theme.form.actionButton}
|
||||
compact
|
||||
>
|
||||
{intl.getMessage('dns_clear_cache')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={showClearConfirm()}>
|
||||
<ConfirmDialog
|
||||
title={intl.getMessage('dns_clear_cache_title')}
|
||||
text={intl.getMessage('dns_clear_cache_desc')}
|
||||
buttonText={intl.getMessage('dns_clear_cache_confirm')}
|
||||
cancelText={intl.getMessage('cancel')}
|
||||
buttonVariant="danger"
|
||||
onClose={() => setShowClearConfirm(false)}
|
||||
onConfirm={() => {
|
||||
clearDnsCache();
|
||||
setShowClearConfirm(false);
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<CacheInputDialog
|
||||
configKey="cache_size"
|
||||
open={cacheSizeDialog.open}
|
||||
onClose={cacheSizeDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<CacheInputDialog
|
||||
configKey="cache_ttl_min"
|
||||
open={minTtlDialog.open}
|
||||
onClose={minTtlDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<CacheInputDialog
|
||||
configKey="cache_ttl_max"
|
||||
open={maxTtlDialog.open}
|
||||
onClose={maxTtlDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,273 +0,0 @@
|
|||
import { createSignal, createMemo, For, Show } from 'solid-js';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { dnsConfigState, clearDnsCache } from 'panel/stores/dnsConfig';
|
||||
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from 'panel/helpers/constants';
|
||||
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 { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { formatNumber } from 'panel/helpers/helpers';
|
||||
|
||||
const CACHE_TTL_MAX_VALUE = 4_294_967_295;
|
||||
|
||||
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 = (props: CacheFormProps) => {
|
||||
const [cacheEnabled, setCacheEnabled] = createSignal(
|
||||
props.initialValues?.cache_enabled || false,
|
||||
);
|
||||
const [cacheSize, setCacheSize] = createSignal(props.initialValues?.cache_size || 0);
|
||||
const [cacheTtlMin, setCacheTtlMin] = createSignal(props.initialValues?.cache_ttl_min || 0);
|
||||
const [cacheTtlMax, setCacheTtlMax] = createSignal(props.initialValues?.cache_ttl_max || 0);
|
||||
const [cacheOptimistic, setCacheOptimistic] = createSignal(
|
||||
props.initialValues?.cache_optimistic || false,
|
||||
);
|
||||
const [openConfirmClear, setOpenConfirmClear] = createSignal(false);
|
||||
|
||||
const minExceedsMax = createMemo(
|
||||
() => cacheTtlMin() > 0 && cacheTtlMax() > 0 && cacheTtlMin() > cacheTtlMax(),
|
||||
);
|
||||
|
||||
const cacheSizeZeroWhenEnabled = createMemo(() => cacheEnabled() && cacheSize() === 0);
|
||||
|
||||
const getFieldValue = (name: string): number => {
|
||||
switch (name) {
|
||||
case CACHE_CONFIG_FIELDS.cache_size:
|
||||
return cacheSize();
|
||||
case CACHE_CONFIG_FIELDS.cache_ttl_min:
|
||||
return cacheTtlMin();
|
||||
case CACHE_CONFIG_FIELDS.cache_ttl_max:
|
||||
return cacheTtlMax();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const setFieldValue = (name: string, value: number) => {
|
||||
switch (name) {
|
||||
case CACHE_CONFIG_FIELDS.cache_size:
|
||||
setCacheSize(value);
|
||||
break;
|
||||
case CACHE_CONFIG_FIELDS.cache_ttl_min:
|
||||
setCacheTtlMin(value);
|
||||
break;
|
||||
case CACHE_CONFIG_FIELDS.cache_ttl_max:
|
||||
setCacheTtlMax(value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCacheInputsValueError = (name: string) => {
|
||||
const value = getFieldValue(name);
|
||||
switch (name) {
|
||||
case CACHE_CONFIG_FIELDS.cache_size: {
|
||||
if (cacheSizeZeroWhenEnabled()) {
|
||||
return (
|
||||
<div class={theme.form.error}>
|
||||
{intl.getMessage('cache_config_size_validation')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (value > CACHE_TTL_MAX_VALUE || value < 0) {
|
||||
return (
|
||||
<div class={theme.form.error}>
|
||||
{intl.getMessage('form_value_length_common_error', {
|
||||
max_length: formatNumber(CACHE_TTL_MAX_VALUE),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CACHE_CONFIG_FIELDS.cache_ttl_min: {
|
||||
if (value > CACHE_TTL_MAX_VALUE || value < 0) {
|
||||
return (
|
||||
<div class={theme.form.error}>
|
||||
{intl.getMessage('form_value_length_common_error', {
|
||||
max_length: formatNumber(CACHE_TTL_MAX_VALUE),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CACHE_CONFIG_FIELDS.cache_ttl_max:
|
||||
if (value > CACHE_TTL_MAX_VALUE || value < 0) {
|
||||
return (
|
||||
<div class={theme.form.error}>
|
||||
{intl.getMessage('form_value_length_common_error', {
|
||||
max_length: formatNumber(CACHE_TTL_MAX_VALUE),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
props.onSubmit({
|
||||
cache_enabled: cacheEnabled(),
|
||||
cache_size: cacheSize(),
|
||||
cache_ttl_min: cacheTtlMin(),
|
||||
cache_ttl_max: cacheTtlMax(),
|
||||
cache_optimistic: cacheOptimistic(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class={theme.form.form}>
|
||||
<div class={theme.form.group}>
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
name="cache_enabled"
|
||||
checked={cacheEnabled()}
|
||||
onChange={(e: Event) =>
|
||||
setCacheEnabled((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
data-testid="dns_cache_enabled"
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>{intl.getMessage('cache_enabled')}</div>
|
||||
<div class={theme.text.t4}>{intl.getMessage('cache_enabled_desc')}</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<For each={INPUTS_FIELDS}>
|
||||
{({ name, title, faq, placeholder }) => (
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={String(getFieldValue(name))}
|
||||
onChange={(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
setFieldValue(name, v === '' ? 0 : Number(v));
|
||||
}}
|
||||
id={name}
|
||||
label={
|
||||
<>
|
||||
{title}
|
||||
<FaqTooltip text={faq} menuSize="large" />
|
||||
</>
|
||||
}
|
||||
placeholder={placeholder}
|
||||
min={0}
|
||||
max={UINT32_RANGE.MAX}
|
||||
/>
|
||||
{handleCacheInputsValueError(name)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={minExceedsMax()}>
|
||||
<div class={theme.form.error}>
|
||||
{intl.getMessage('cache_config_ttl_validation')}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
name="cache_optimistic"
|
||||
checked={cacheOptimistic()}
|
||||
onChange={(e: Event) =>
|
||||
setCacheOptimistic((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
data-testid="dns_cache_optimistic"
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>
|
||||
{intl.getMessage('cache_config_optimistic')}
|
||||
</div>
|
||||
<div class={theme.text.t4}>
|
||||
{intl.getMessage('cache_config_optimistic_desc')}
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.buttonGroup}>
|
||||
<Button
|
||||
type="submit"
|
||||
id="dns_save"
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={
|
||||
dnsConfigState.processingSetConfig ||
|
||||
minExceedsMax() ||
|
||||
cacheSizeZeroWhenEnabled()
|
||||
}
|
||||
class={theme.form.button}
|
||||
>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
id="dns_clear"
|
||||
variant="secondary-danger"
|
||||
size="small"
|
||||
onClick={() => setOpenConfirmClear(true)}
|
||||
class={theme.form.button}
|
||||
>
|
||||
{intl.getMessage('cache_config_clear')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={openConfirmClear()}>
|
||||
<ConfirmDialog
|
||||
onClose={() => setOpenConfirmClear(false)}
|
||||
onConfirm={() => {
|
||||
clearDnsCache();
|
||||
setOpenConfirmClear(false);
|
||||
}}
|
||||
buttonText={intl.getMessage('confirm')}
|
||||
cancelText={intl.getMessage('cancel')}
|
||||
title={intl.getMessage('cache_confirm_clear_title')}
|
||||
text={intl.getMessage('cache_confirm_clear_desc')}
|
||||
buttonVariant="danger"
|
||||
/>
|
||||
</Show>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { UINT32_RANGE } from 'panel/helpers/constants';
|
||||
import { validateBetween, validateRequiredValue } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type CacheConfigKey = 'cache_size' | 'cache_ttl_min' | 'cache_ttl_max';
|
||||
|
||||
type Props = {
|
||||
configKey: CacheConfigKey;
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
const CACHE_DIALOG_CONFIG: Record<
|
||||
CacheConfigKey,
|
||||
{
|
||||
title: () => string;
|
||||
description: () => string;
|
||||
label: () => string;
|
||||
placeholder?: () => string;
|
||||
validate: (v: string) => string;
|
||||
}
|
||||
> = {
|
||||
cache_size: {
|
||||
title: () => intl.getMessage('dns_cache_size_title'),
|
||||
description: () => intl.getMessage('dns_cache_size_desc'),
|
||||
label: () => intl.getMessage('dns_cache_size_label'),
|
||||
validate: (v) =>
|
||||
validateRequiredValue(v) || validateBetween(Number(v), UINT32_RANGE.MIN, UINT32_RANGE.MAX) || '',
|
||||
},
|
||||
cache_ttl_min: {
|
||||
title: () => intl.getMessage('dns_override_min_ttl_title'),
|
||||
description: () => intl.getMessage('dns_override_min_ttl_desc'),
|
||||
label: () => intl.getMessage('dns_override_min_ttl_label'),
|
||||
placeholder: () => intl.getMessage('dns_override_min_ttl_placeholder'),
|
||||
validate: (v) =>
|
||||
validateRequiredValue(v) || validateBetween(Number(v), UINT32_RANGE.MIN, UINT32_RANGE.MAX) || '',
|
||||
},
|
||||
cache_ttl_max: {
|
||||
title: () => intl.getMessage('dns_override_max_ttl_title'),
|
||||
description: () => intl.getMessage('dns_override_max_ttl_desc'),
|
||||
label: () => intl.getMessage('dns_override_max_ttl_label'),
|
||||
placeholder: () => intl.getMessage('dns_override_max_ttl_placeholder'),
|
||||
validate: (v) => {
|
||||
const requiredErr = validateRequiredValue(v);
|
||||
if (requiredErr) return requiredErr;
|
||||
const num = Number(v);
|
||||
const rangeErr = validateBetween(num, UINT32_RANGE.MIN, UINT32_RANGE.MAX);
|
||||
if (rangeErr) return rangeErr;
|
||||
// Cross-field TTL check: min > max
|
||||
const minVal = Number(dnsConfigState.cache_ttl_min);
|
||||
if (minVal > 0 && num > 0 && minVal > num) {
|
||||
return intl.getMessage('cache_config_ttl_validation');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CacheInputDialog = (props: Props) => {
|
||||
const config = () => CACHE_DIALOG_CONFIG[props.configKey];
|
||||
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => String(dnsConfigState[props.configKey] ?? 0),
|
||||
{ validate: config().validate },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={config().title()}
|
||||
description={config().description()}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
const configKey = props.configKey;
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ [configKey]: v === '' ? 0 : Number(v) });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLInputElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id={props.configKey}
|
||||
label={config().label()}
|
||||
placeholder={config().placeholder?.()}
|
||||
min={UINT32_RANGE.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
errorMessage={field.error()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 12px;
|
||||
font-size: var(--fs-text-t2);
|
||||
line-height: var(--lh-t2-normal);
|
||||
color: var(--default-main-text);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import cn from 'clsx';
|
||||
|
||||
import {
|
||||
dnsConfigState,
|
||||
togglePrivatePtrResolvers,
|
||||
toggleResolveClients,
|
||||
} from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { Breadcrumbs } from 'panel/common/ui/Breadcrumbs';
|
||||
import { SettingRow } from 'panel/common/ui/SettingRow';
|
||||
import { RoutePath } from 'panel/components/Routes/Paths';
|
||||
import { useDialogOpen } from 'panel/hooks/useField';
|
||||
import { PrivateReverseServersDialog } from '../Upstream/blocks/PrivateReverseServersDialog';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './PrivateReverse.module.pcss';
|
||||
|
||||
export const PrivateReverse = () => {
|
||||
const serversDialog = useDialogOpen();
|
||||
|
||||
const processing = () => dnsConfigState.processingSetConfig;
|
||||
|
||||
return (
|
||||
<div class={cn(theme.layout.container, s.container)}>
|
||||
<div class={cn(theme.layout.containerIn, theme.layout.containerIn_one_col)}>
|
||||
<Breadcrumbs
|
||||
parentLinks={[
|
||||
{
|
||||
path: RoutePath.Dns,
|
||||
title: intl.getMessage('dns_settings'),
|
||||
},
|
||||
]}
|
||||
currentTitle={intl.getMessage('dns_private_reverse_resolvers')}
|
||||
/>
|
||||
|
||||
<div class={s.form}>
|
||||
<SettingRow
|
||||
variant="switch"
|
||||
id="use_private_ptr_resolvers"
|
||||
title={intl.getMessage('dns_private_reverse_resolvers')}
|
||||
titleClass={cn(theme.title.h4, theme.title.h3_tablet, s.title)}
|
||||
description={
|
||||
<>
|
||||
<p class={s.desc}>
|
||||
{intl.getMessage('dns_private_reverse_resolvers_desc')}
|
||||
</p>
|
||||
<p class={s.desc}>
|
||||
{intl.getMessage('dns_private_reverse_resolvers_disabled_desc')}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
checked={dnsConfigState.use_private_ptr_resolvers}
|
||||
onChange={() => togglePrivatePtrResolvers()}
|
||||
largeTitle
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="private_reverse_servers"
|
||||
title={intl.getMessage('dns_private_reverse_servers_title')}
|
||||
description={intl.getMessage('dns_private_reverse_servers_desc')}
|
||||
onClick={serversDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="switch"
|
||||
id="resolve_clients"
|
||||
title={intl.getMessage('dns_private_reverse_resolve_clients_title')}
|
||||
description={intl.getMessage('dns_private_reverse_resolve_clients_desc')}
|
||||
checked={dnsConfigState.resolve_clients}
|
||||
onChange={() => toggleResolveClients()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PrivateReverseServersDialog
|
||||
open={serversDialog.open}
|
||||
onClose={serversDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { PrivateReverse } from './PrivateReverse';
|
||||
|
|
@ -1,14 +1,40 @@
|
|||
import { createMemo } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import {
|
||||
dnsConfigState,
|
||||
toggleDnssecEnabled,
|
||||
toggleDisableIPv6,
|
||||
toggleEdnsCsEnabled,
|
||||
} from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { SettingRow } from 'panel/common/ui/SettingRow';
|
||||
import { getRateLimitSummary, getBlockingModeSummary, getUpstreamServersSummary } from '../helpers';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Form } from './blocks/Form';
|
||||
import { IP_VERSION, IPV4_SUBNET_PREFIX, IPV6_SUBNET_PREFIX } from 'panel/helpers/constants';
|
||||
import { useDialogOpen } from 'panel/hooks/useField';
|
||||
|
||||
import { RateLimitDialog } from './blocks/RateLimitDialog';
|
||||
import { SubnetPrefixV4Dialog } from './blocks/SubnetPrefixV4Dialog';
|
||||
import { SubnetPrefixV6Dialog } from './blocks/SubnetPrefixV6Dialog';
|
||||
import { BlockingModeDialog } from './blocks/BlockingModeDialog';
|
||||
import { RateLimitAllowlistDialog } from './blocks/RateLimitAllowlistDialog';
|
||||
import { EdnsDialog } from './blocks/EdnsDialog';
|
||||
|
||||
export const ServerConfig = () => {
|
||||
const handleFormSubmit = (values: any) => {
|
||||
setDnsConfig(values, intl.getMessage('dns_server_configuration_saved_toast'));
|
||||
};
|
||||
const rateLimitDialog = useDialogOpen();
|
||||
const subnetV4Dialog = useDialogOpen();
|
||||
const subnetV6Dialog = useDialogOpen();
|
||||
const blockingModeDialog = useDialogOpen();
|
||||
const allowlistDialog = useDialogOpen();
|
||||
const ednsDialog = useDialogOpen();
|
||||
|
||||
const rateLimitValue = createMemo(() => getRateLimitSummary(dnsConfigState.ratelimit));
|
||||
const blockingModeValue = createMemo(() =>
|
||||
getBlockingModeSummary(dnsConfigState.blocking_mode),
|
||||
);
|
||||
|
||||
const processing = () => dnsConfigState.processingSetConfig;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -16,24 +42,118 @@ export const ServerConfig = () => {
|
|||
{intl.getMessage('dns_config')}
|
||||
</h2>
|
||||
|
||||
<Form
|
||||
initialValues={{
|
||||
ratelimit: dnsConfigState.ratelimit,
|
||||
ratelimit_subnet_len_ipv4: dnsConfigState.ratelimit_subnet_len_ipv4,
|
||||
ratelimit_subnet_len_ipv6: dnsConfigState.ratelimit_subnet_len_ipv6,
|
||||
ratelimit_whitelist: dnsConfigState.ratelimit_whitelist,
|
||||
blocking_mode: dnsConfigState.blocking_mode,
|
||||
blocking_ipv4: dnsConfigState.blocking_ipv4,
|
||||
blocking_ipv6: dnsConfigState.blocking_ipv6,
|
||||
blocked_response_ttl: dnsConfigState.blocked_response_ttl,
|
||||
edns_cs_enabled: dnsConfigState.edns_cs_enabled,
|
||||
disable_ipv6: dnsConfigState.disable_ipv6,
|
||||
dnssec_enabled: dnsConfigState.dnssec_enabled,
|
||||
edns_cs_use_custom: dnsConfigState.edns_cs_use_custom,
|
||||
edns_cs_custom_ip: dnsConfigState.edns_cs_custom_ip,
|
||||
}}
|
||||
onSubmit={handleFormSubmit}
|
||||
processing={dnsConfigState.processingSetConfig}
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="rate_limit"
|
||||
title={intl.getMessage('dns_rate_limit')}
|
||||
description={intl.getMessage('dns_rate_limit_desc')}
|
||||
value={rateLimitValue()}
|
||||
onClick={rateLimitDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="subnet_ipv4"
|
||||
title={intl.getMessage('dns_subnet_prefix', { value: IP_VERSION.V4 })}
|
||||
description={intl.getMessage('dns_subnet_prefix_desc', { value: IP_VERSION.V4 })}
|
||||
value={String(
|
||||
dnsConfigState.ratelimit_subnet_len_ipv4 ?? IPV4_SUBNET_PREFIX.DEFAULT,
|
||||
)}
|
||||
onClick={subnetV4Dialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="subnet_ipv6"
|
||||
title={intl.getMessage('dns_subnet_prefix', { value: IP_VERSION.V6 })}
|
||||
description={intl.getMessage('dns_subnet_prefix_desc', { value: IP_VERSION.V6 })}
|
||||
value={String(
|
||||
dnsConfigState.ratelimit_subnet_len_ipv6 ?? IPV6_SUBNET_PREFIX.DEFAULT,
|
||||
)}
|
||||
onClick={subnetV6Dialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="blocking_mode"
|
||||
title={intl.getMessage('dns_blocking_mode')}
|
||||
description={intl.getMessage('dns_blocking_mode_desc')}
|
||||
value={blockingModeValue()}
|
||||
onClick={blockingModeDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="ratelimit_allowlist"
|
||||
title={intl.getMessage('dns_rate_limit_allowlist')}
|
||||
description={intl.getMessage('dns_rate_limit_allowlist_desc')}
|
||||
value={getUpstreamServersSummary(dnsConfigState.ratelimit_whitelist)}
|
||||
onClick={allowlistDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="switch-link"
|
||||
id="edns_client_subnet"
|
||||
title={intl.getMessage('dns_edns_client_subnet')}
|
||||
description={intl.getMessage('dns_edns_client_subnet_desc')}
|
||||
checked={dnsConfigState.edns_cs_enabled}
|
||||
onChange={() => toggleEdnsCsEnabled()}
|
||||
onClick={ednsDialog.openDialog}
|
||||
divider
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="switch"
|
||||
id="dnssec"
|
||||
title={intl.getMessage('dns_dnssec')}
|
||||
description={intl.getMessage('dns_dnssec_desc')}
|
||||
checked={dnsConfigState.dnssec_enabled}
|
||||
onChange={() => toggleDnssecEnabled()}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="switch"
|
||||
id="ipv6_resolution"
|
||||
title={intl.getMessage('dns_ipv6_resolution')}
|
||||
description={intl.getMessage('dns_ipv6_resolution_desc')}
|
||||
checked={!dnsConfigState.disable_ipv6}
|
||||
onChange={() => toggleDisableIPv6()}
|
||||
/>
|
||||
|
||||
<RateLimitDialog
|
||||
open={rateLimitDialog.open}
|
||||
onClose={rateLimitDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<SubnetPrefixV4Dialog
|
||||
open={subnetV4Dialog.open}
|
||||
onClose={subnetV4Dialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<SubnetPrefixV6Dialog
|
||||
open={subnetV6Dialog.open}
|
||||
onClose={subnetV6Dialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<BlockingModeDialog
|
||||
open={blockingModeDialog.open}
|
||||
onClose={blockingModeDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<RateLimitAllowlistDialog
|
||||
open={allowlistDialog.open}
|
||||
onClose={allowlistDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
|
||||
<EdnsDialog
|
||||
open={ednsDialog.open}
|
||||
onClose={ednsDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
import { createSignal, createEffect, type Accessor, Show } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import { BLOCKING_MODES, UINT32_RANGE, getBlockingModeOptions } from 'panel/helpers/constants';
|
||||
import {
|
||||
validateRequiredValue,
|
||||
validateBetween,
|
||||
validateIpv4,
|
||||
validateIpv6,
|
||||
} from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const BlockingModeDialog = (props: Props) => {
|
||||
const blockingModeOptions = getBlockingModeOptions();
|
||||
|
||||
const [blockingMode, setBlockingMode] = createSignal(dnsConfigState.blocking_mode);
|
||||
createEffect(() => {
|
||||
if (props.open()) {
|
||||
setBlockingMode(dnsConfigState.blocking_mode);
|
||||
}
|
||||
});
|
||||
|
||||
const blockingIpv4 = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.blocking_ipv4,
|
||||
{
|
||||
validate: (v) => validateRequiredValue(v) || validateIpv4(v) || '',
|
||||
},
|
||||
);
|
||||
const blockingIpv6 = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.blocking_ipv6,
|
||||
{
|
||||
validate: (v) => validateRequiredValue(v) || validateIpv6(v) || '',
|
||||
},
|
||||
);
|
||||
const ttl = useField<number>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.blocked_response_ttl,
|
||||
{
|
||||
validate: (v) =>
|
||||
validateRequiredValue(String(v)) ||
|
||||
validateBetween(v, UINT32_RANGE.MIN, UINT32_RANGE.MAX) ||
|
||||
'',
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const isCustom = blockingMode() === BLOCKING_MODES.custom_ip;
|
||||
if (isCustom && (blockingIpv4.validate() || blockingIpv6.validate())) return;
|
||||
if (ttl.validate()) return;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
blocking_mode: blockingMode(),
|
||||
};
|
||||
if (blockingMode() === BLOCKING_MODES.custom_ip) {
|
||||
payload.blocking_ipv4 = blockingIpv4.value();
|
||||
payload.blocking_ipv6 = blockingIpv6.value();
|
||||
payload.blocked_response_ttl = ttl.value();
|
||||
}
|
||||
setDnsConfig(payload);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const submitDisabled = () => {
|
||||
if (blockingMode() === BLOCKING_MODES.custom_ip) {
|
||||
return !!(blockingIpv4.error() || blockingIpv6.error() || ttl.error());
|
||||
}
|
||||
return !!ttl.error();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_blocking_mode_title')}
|
||||
description={intl.getMessage('dns_blocking_mode_desc')}
|
||||
onClose={props.onClose}
|
||||
onSubmit={handleSubmit}
|
||||
processing={props.processing}
|
||||
submitDisabled={submitDisabled()}
|
||||
>
|
||||
<Radio
|
||||
name="blocking_mode"
|
||||
options={blockingModeOptions}
|
||||
value={blockingMode()}
|
||||
handleChange={(v: string) => setBlockingMode(v)}
|
||||
inModal
|
||||
/>
|
||||
<Show when={blockingMode() === BLOCKING_MODES.custom_ip}>
|
||||
<>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
id="blocking_ipv4"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('dns_blocking_mode_ipv4_label')}
|
||||
<FaqTooltip
|
||||
text={intl.getMessage(
|
||||
'server_config_blocking_mode_ipv4_faq',
|
||||
)}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={intl.getMessage('dns_blocking_mode_ipv4_placeholder')}
|
||||
value={blockingIpv4.value()}
|
||||
onChange={(e: Event) =>
|
||||
blockingIpv4.setValue((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onBlur={() => blockingIpv4.validate()}
|
||||
errorMessage={blockingIpv4.error()}
|
||||
disabled={blockingMode() !== BLOCKING_MODES.custom_ip}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
id="blocking_ipv6"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('dns_blocking_mode_ipv6_label')}
|
||||
<FaqTooltip
|
||||
text={intl.getMessage(
|
||||
'server_config_blocking_mode_ipv6_faq',
|
||||
)}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={intl.getMessage('dns_blocking_mode_ipv6_placeholder')}
|
||||
value={blockingIpv6.value()}
|
||||
onChange={(e: Event) =>
|
||||
blockingIpv6.setValue((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onBlur={() => blockingIpv6.validate()}
|
||||
errorMessage={blockingIpv6.error()}
|
||||
disabled={blockingMode() !== BLOCKING_MODES.custom_ip}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
id="blocked_response_ttl"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('dns_blocking_mode_ttl_label')}
|
||||
<FaqTooltip
|
||||
text={intl.getMessage('server_config_blocking_mode_ttl_faq')}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={intl.getMessage('dns_blocking_mode_ttl_placeholder')}
|
||||
value={ttl.value()}
|
||||
onChange={(e: Event) =>
|
||||
ttl.setValue(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() => ttl.validate()}
|
||||
min={UINT32_RANGE.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
errorMessage={ttl.error()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { createSignal, createEffect, type Accessor } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import { EDNS_MODES, getEdnsOptions } from 'panel/helpers/constants';
|
||||
import { validateRequiredValue, validateIp } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const EdnsDialog = (props: Props) => {
|
||||
const ednsOptions = getEdnsOptions();
|
||||
|
||||
// EDNS custom flag — plain signal synced from store
|
||||
const [ednsCsUseCustom, setEdnsCsUseCustom] = createSignal(
|
||||
dnsConfigState.edns_cs_use_custom ?? false,
|
||||
);
|
||||
createEffect(() => {
|
||||
if (props.open()) {
|
||||
setEdnsCsUseCustom(dnsConfigState.edns_cs_use_custom ?? false);
|
||||
}
|
||||
});
|
||||
|
||||
// Custom IP field — conditionally validated
|
||||
const ednsCsCustomIp = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.edns_cs_custom_ip ?? '',
|
||||
{
|
||||
validate: (v) =>
|
||||
ednsCsUseCustom() ? validateRequiredValue(v) || validateIp(v) || '' : '',
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Bugfix: submit-time validation
|
||||
if (ednsCsUseCustom() && ednsCsCustomIp.validate()) return;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
edns_cs_use_custom: ednsCsUseCustom(),
|
||||
};
|
||||
if (ednsCsUseCustom()) {
|
||||
payload.edns_cs_custom_ip = ednsCsCustomIp.value();
|
||||
}
|
||||
setDnsConfig(payload);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_edns_title')}
|
||||
description={intl.getMessage('dns_edns_desc')}
|
||||
onClose={props.onClose}
|
||||
onSubmit={handleSubmit}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!(ednsCsUseCustom() && ednsCsCustomIp.error())}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Radio
|
||||
name="edns_cs_mode"
|
||||
options={ednsOptions}
|
||||
value={ednsCsUseCustom() ? EDNS_MODES.custom : EDNS_MODES.default}
|
||||
handleChange={(v: string) => setEdnsCsUseCustom(v === EDNS_MODES.custom)}
|
||||
inModal
|
||||
/>
|
||||
<Input
|
||||
id="edns_cs_custom_ip"
|
||||
label={intl.getMessage('dns_edns_custom_label')}
|
||||
placeholder={intl.getMessage('dns_edns_custom_placeholder')}
|
||||
value={ednsCsCustomIp.value()}
|
||||
onChange={(e: Event) =>
|
||||
ednsCsCustomIp.setValue((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onBlur={() => ednsCsCustomIp.validate()}
|
||||
disabled={!ednsCsUseCustom()}
|
||||
errorMessage={ednsCsCustomIp.error()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.subtitle {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.descriptions {
|
||||
padding: 12px 0 8px;
|
||||
}
|
||||
|
|
@ -1,565 +0,0 @@
|
|||
import { createSignal, createMemo, For, Show } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import {
|
||||
validateBetween,
|
||||
validateIp,
|
||||
validateIpPerLine,
|
||||
validateIpv4,
|
||||
validateIpv6,
|
||||
validateMaxValue,
|
||||
validateRequiredValue,
|
||||
} from 'panel/helpers/validators';
|
||||
import {
|
||||
BLOCKING_MODES,
|
||||
IPV4_SUBNET_PREFIX,
|
||||
IPV6_SUBNET_PREFIX,
|
||||
RATE_LIMIT,
|
||||
UINT32_RANGE,
|
||||
} from 'panel/helpers/constants';
|
||||
import { Checkbox } from 'panel/common/controls/Checkbox';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
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 = (props: Props) => {
|
||||
const [ratelimit, setRatelimit] = createSignal(props.initialValues?.ratelimit ?? 0);
|
||||
const [ratelimitSubnetIpv4, setRatelimitSubnetIpv4] = createSignal(
|
||||
props.initialValues?.ratelimit_subnet_len_ipv4 ?? 0,
|
||||
);
|
||||
const [ratelimitSubnetIpv6, setRatelimitSubnetIpv6] = createSignal(
|
||||
props.initialValues?.ratelimit_subnet_len_ipv6 ?? 0,
|
||||
);
|
||||
const [ratelimitWhitelist, setRatelimitWhitelist] = createSignal(
|
||||
props.initialValues?.ratelimit_whitelist ?? '',
|
||||
);
|
||||
const [ednsCsEnabled, setEdnsCsEnabled] = createSignal(
|
||||
props.initialValues?.edns_cs_enabled ?? false,
|
||||
);
|
||||
const [ednsCsUseCustom, setEdnsCsUseCustom] = createSignal(
|
||||
props.initialValues?.edns_cs_use_custom ?? false,
|
||||
);
|
||||
const [ednsCsCustomIp, setEdnsCsCustomIp] = createSignal(
|
||||
props.initialValues?.edns_cs_custom_ip ?? '',
|
||||
);
|
||||
const [dnssecEnabled, setDnssecEnabled] = createSignal(
|
||||
props.initialValues?.dnssec_enabled ?? false,
|
||||
);
|
||||
const [disableIpv6, setDisableIpv6] = createSignal(props.initialValues?.disable_ipv6 ?? false);
|
||||
const [blockingMode, setBlockingMode] = createSignal(
|
||||
props.initialValues?.blocking_mode ?? BLOCKING_MODES.default,
|
||||
);
|
||||
const [blockingIpv4, setBlockingIpv4] = createSignal(props.initialValues?.blocking_ipv4 ?? '');
|
||||
const [blockingIpv6, setBlockingIpv6] = createSignal(props.initialValues?.blocking_ipv6 ?? '');
|
||||
const [blockedResponseTtl, setBlockedResponseTtl] = createSignal(
|
||||
props.initialValues?.blocked_response_ttl ?? 0,
|
||||
);
|
||||
|
||||
// Error signals
|
||||
const [ratelimitError, setRatelimitError] = createSignal('');
|
||||
const [subnetIpv4Error, setSubnetIpv4Error] = createSignal('');
|
||||
const [subnetIpv6Error, setSubnetIpv6Error] = createSignal('');
|
||||
const [whitelistError, setWhitelistError] = createSignal('');
|
||||
const [customIpError, setCustomIpError] = createSignal('');
|
||||
const [blockingIpv4Error, setBlockingIpv4Error] = createSignal('');
|
||||
const [blockingIpv6Error, setBlockingIpv6Error] = createSignal('');
|
||||
const [ttlError, setTtlError] = createSignal('');
|
||||
|
||||
const validateAll = () => {
|
||||
setRatelimitError(
|
||||
validateRequiredValue(String(ratelimit())) ||
|
||||
validateMaxValue(ratelimit(), RATE_LIMIT.MAX) ||
|
||||
'',
|
||||
);
|
||||
setSubnetIpv4Error(
|
||||
validateRequiredValue(String(ratelimitSubnetIpv4())) ||
|
||||
validateBetween(
|
||||
ratelimitSubnetIpv4(),
|
||||
IPV4_SUBNET_PREFIX.MIN,
|
||||
IPV4_SUBNET_PREFIX.MAX,
|
||||
) ||
|
||||
'',
|
||||
);
|
||||
setSubnetIpv6Error(
|
||||
validateRequiredValue(String(ratelimitSubnetIpv6())) ||
|
||||
validateBetween(
|
||||
ratelimitSubnetIpv6(),
|
||||
IPV6_SUBNET_PREFIX.MIN,
|
||||
IPV6_SUBNET_PREFIX.MAX,
|
||||
) ||
|
||||
'',
|
||||
);
|
||||
setWhitelistError(
|
||||
ratelimitWhitelist() ? validateIpPerLine(ratelimitWhitelist()) || '' : '',
|
||||
);
|
||||
setTtlError(
|
||||
validateRequiredValue(String(blockedResponseTtl())) ||
|
||||
validateBetween(blockedResponseTtl(), UINT32_RANGE.MIN, UINT32_RANGE.MAX) ||
|
||||
'',
|
||||
);
|
||||
|
||||
if (ednsCsUseCustom()) {
|
||||
const err =
|
||||
validateRequiredValue(ednsCsCustomIp()) || validateIp(ednsCsCustomIp()) || '';
|
||||
setCustomIpError(err);
|
||||
}
|
||||
|
||||
if (blockingMode() === BLOCKING_MODES.custom_ip) {
|
||||
setBlockingIpv4Error(
|
||||
validateRequiredValue(blockingIpv4()) || validateIpv4(blockingIpv4()) || '',
|
||||
);
|
||||
setBlockingIpv6Error(
|
||||
validateRequiredValue(blockingIpv6()) || validateIpv6(blockingIpv6()) || '',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const hasErrors = createMemo(
|
||||
() =>
|
||||
ratelimitError() ||
|
||||
subnetIpv4Error() ||
|
||||
subnetIpv6Error() ||
|
||||
whitelistError() ||
|
||||
customIpError() ||
|
||||
blockingIpv4Error() ||
|
||||
blockingIpv6Error() ||
|
||||
ttlError(),
|
||||
);
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
validateAll();
|
||||
if (hasErrors()) return;
|
||||
|
||||
props.onSubmit({
|
||||
ratelimit: ratelimit(),
|
||||
ratelimit_subnet_len_ipv4: ratelimitSubnetIpv4(),
|
||||
ratelimit_subnet_len_ipv6: ratelimitSubnetIpv6(),
|
||||
ratelimit_whitelist: ratelimitWhitelist(),
|
||||
edns_cs_enabled: ednsCsEnabled(),
|
||||
edns_cs_use_custom: ednsCsUseCustom(),
|
||||
edns_cs_custom_ip: ednsCsCustomIp(),
|
||||
dnssec_enabled: dnssecEnabled(),
|
||||
disable_ipv6: disableIpv6(),
|
||||
blocking_mode: blockingMode(),
|
||||
blocking_ipv4: blockingIpv4(),
|
||||
blocking_ipv6: blockingIpv6(),
|
||||
blocked_response_ttl: blockedResponseTtl(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class={theme.form.form}>
|
||||
<div class={theme.form.group}>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={String(ratelimit())}
|
||||
onChange={(e: Event) =>
|
||||
setRatelimit(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() =>
|
||||
setRatelimitError(
|
||||
validateRequiredValue(String(ratelimit())) ||
|
||||
validateMaxValue(ratelimit(), RATE_LIMIT.MAX) ||
|
||||
'',
|
||||
)
|
||||
}
|
||||
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={ratelimitError()}
|
||||
min={UINT32_RANGE.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={String(ratelimitSubnetIpv4())}
|
||||
onChange={(e: Event) =>
|
||||
setRatelimitSubnetIpv4(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() =>
|
||||
setSubnetIpv4Error(
|
||||
validateRequiredValue(String(ratelimitSubnetIpv4())) ||
|
||||
validateBetween(
|
||||
ratelimitSubnetIpv4(),
|
||||
IPV4_SUBNET_PREFIX.MIN,
|
||||
IPV4_SUBNET_PREFIX.MAX,
|
||||
) ||
|
||||
'',
|
||||
)
|
||||
}
|
||||
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={subnetIpv4Error()}
|
||||
min={0}
|
||||
max={32}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={String(ratelimitSubnetIpv6())}
|
||||
onChange={(e: Event) =>
|
||||
setRatelimitSubnetIpv6(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() =>
|
||||
setSubnetIpv6Error(
|
||||
validateRequiredValue(String(ratelimitSubnetIpv6())) ||
|
||||
validateBetween(
|
||||
ratelimitSubnetIpv6(),
|
||||
IPV6_SUBNET_PREFIX.MIN,
|
||||
IPV6_SUBNET_PREFIX.MAX,
|
||||
) ||
|
||||
'',
|
||||
)
|
||||
}
|
||||
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={subnetIpv6Error()}
|
||||
min={0}
|
||||
max={128}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={ratelimitWhitelist()}
|
||||
onChange={(e: Event) =>
|
||||
setRatelimitWhitelist((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={() =>
|
||||
setWhitelistError(
|
||||
ratelimitWhitelist()
|
||||
? validateIpPerLine(ratelimitWhitelist()) || ''
|
||||
: '',
|
||||
)
|
||||
}
|
||||
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={whitelistError()}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
name="edns_cs_enabled"
|
||||
checked={ednsCsEnabled()}
|
||||
onChange={(e: Event) =>
|
||||
setEdnsCsEnabled((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
data-testid="dns_config_edns_cs_enabled"
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>
|
||||
{intl.getMessage('server_config_edns_enable')}
|
||||
</div>
|
||||
<div class={theme.text.t4}>
|
||||
{intl.getMessage('server_config_edns_cs_desc')}
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.inner}>
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
name="edns_cs_use_custom"
|
||||
checked={ednsCsUseCustom()}
|
||||
onChange={(e: Event) =>
|
||||
setEdnsCsUseCustom((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
data-testid="dns_config_edns_use_custom_ip"
|
||||
disabled={!ednsCsEnabled()}
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>
|
||||
{intl.getMessage('server_config_edns_use_custom_ip')}
|
||||
</div>
|
||||
<div class={theme.text.t4}>
|
||||
{intl.getMessage('server_config_edns_use_custom_ip_desc')}
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<Show when={ednsCsUseCustom()}>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={ednsCsCustomIp()}
|
||||
onChange={(e: Event) =>
|
||||
setEdnsCsCustomIp((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onBlur={() => {
|
||||
if (ednsCsUseCustom()) {
|
||||
setCustomIpError(
|
||||
validateRequiredValue(ednsCsCustomIp()) ||
|
||||
validateIp(ednsCsCustomIp()) ||
|
||||
'',
|
||||
);
|
||||
}
|
||||
}}
|
||||
data-testid="dns_config_edns_cs_custom_ip"
|
||||
placeholder={intl.getMessage('enter_ip_address_placeholder')}
|
||||
errorMessage={customIpError()}
|
||||
disabled={!ednsCsEnabled()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<For each={checkboxes}>
|
||||
{({ name, placeholder, subtitle }) => (
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={
|
||||
name === 'dnssec_enabled' ? dnssecEnabled() : disableIpv6()
|
||||
}
|
||||
onChange={(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).checked;
|
||||
if (name === 'dnssec_enabled') setDnssecEnabled(v);
|
||||
else setDisableIpv6(v);
|
||||
}}
|
||||
id={`dns_config_${name}`}
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>{placeholder}</div>
|
||||
<div class={theme.text.t4}>{subtitle}</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<div class={cn(s.subtitle, theme.title.h6)}>
|
||||
{intl.getMessage('server_config_blocking_mode')}
|
||||
</div>
|
||||
<div class={s.descriptions}>
|
||||
<For each={blockingModesDescriptions}>
|
||||
{(description) => <div class={theme.text.t2}>{description}</div>}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Radio
|
||||
value={blockingMode()}
|
||||
handleChange={(v: string) => setBlockingMode(v)}
|
||||
name="blocking_mode"
|
||||
options={blockingModeOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={blockingMode() === BLOCKING_MODES.custom_ip}>
|
||||
<For each={customIps}>
|
||||
{({ label, name, placeholder, faq, validateIp: validateIpFn }) => (
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={
|
||||
name === 'blocking_ipv4' ? blockingIpv4() : blockingIpv6()
|
||||
}
|
||||
onChange={(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
if (name === 'blocking_ipv4') setBlockingIpv4(v);
|
||||
else setBlockingIpv6(v);
|
||||
}}
|
||||
onBlur={() => {
|
||||
const val =
|
||||
name === 'blocking_ipv4'
|
||||
? blockingIpv4()
|
||||
: blockingIpv6();
|
||||
const err =
|
||||
validateRequiredValue(val) || validateIpFn(val) || '';
|
||||
if (name === 'blocking_ipv4') setBlockingIpv4Error(err);
|
||||
else setBlockingIpv6Error(err);
|
||||
}}
|
||||
data-testid={`dns_config_${name}`}
|
||||
type="text"
|
||||
label={
|
||||
<>
|
||||
{label}
|
||||
<FaqTooltip text={faq} menuSize="large" />
|
||||
</>
|
||||
}
|
||||
placeholder={placeholder}
|
||||
errorMessage={
|
||||
name === 'blocking_ipv4'
|
||||
? blockingIpv4Error()
|
||||
: blockingIpv6Error()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={String(blockedResponseTtl())}
|
||||
onChange={(e: Event) =>
|
||||
setBlockedResponseTtl(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() =>
|
||||
setTtlError(validateRequiredValue(String(blockedResponseTtl())) || '')
|
||||
}
|
||||
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={ttlError()}
|
||||
min={UINT32_RANGE.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.buttonGroup}>
|
||||
<Button
|
||||
type="submit"
|
||||
id="dns_config_save"
|
||||
variant="primary"
|
||||
disabled={props.processing}
|
||||
class={theme.form.button}
|
||||
size="small"
|
||||
>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Form } from './Form';
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateIpPerLine } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const RateLimitAllowlistDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.ratelimit_whitelist,
|
||||
{
|
||||
validate: (v) => (v ? validateIpPerLine(v) || '' : ''),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_rate_limit_allowlist_title')}
|
||||
description={intl.getMessage('dns_rate_limit_allowlist_desc')}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ ratelimit_whitelist: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id="ratelimit_whitelist"
|
||||
label={intl.getMessage('dns_rate_limit_allowlist_label')}
|
||||
placeholder={intl.getMessage('dns_rate_limit_allowlist_placeholder')}
|
||||
size="medium"
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { RATE_LIMIT } from 'panel/helpers/constants';
|
||||
import { validateRequiredValue, validateMaxValue } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const RateLimitDialog = (props: Props) => {
|
||||
const field = useField<number>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.ratelimit,
|
||||
{
|
||||
validate: (v) =>
|
||||
validateRequiredValue(String(v)) || validateMaxValue(v, RATE_LIMIT.MAX) || '',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_rate_limit_title')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_rate_limit_desc')}</p>
|
||||
<p>{intl.getMessage('dns_rate_limit_desc_2')}</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ ratelimit: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value()}
|
||||
onChange={(e: Event) =>
|
||||
field.setValue(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() => field.validate()}
|
||||
id="ratelimit"
|
||||
label={intl.getMessage('server_config_rate_limit')}
|
||||
placeholder={intl.getMessage('dns_rate_limit_placeholder')}
|
||||
min={RATE_LIMIT.MIN}
|
||||
max={RATE_LIMIT.MAX}
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { IPV4_SUBNET_PREFIX, IPV6_SUBNET_PREFIX, IP_VERSION } from 'panel/helpers/constants';
|
||||
import { validateRequiredValue, validateBetween } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type SubnetVersion = 'ipv4' | 'ipv6';
|
||||
|
||||
type Props = {
|
||||
version: SubnetVersion;
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const SubnetPrefixDialog = (props: Props) => {
|
||||
const isV4 = () => props.version === 'ipv4';
|
||||
const range = () => (isV4() ? IPV4_SUBNET_PREFIX : IPV6_SUBNET_PREFIX);
|
||||
|
||||
const field = useField<number>(
|
||||
() => props.open(),
|
||||
() =>
|
||||
isV4()
|
||||
? (dnsConfigState.ratelimit_subnet_len_ipv4 ?? IPV4_SUBNET_PREFIX.DEFAULT)
|
||||
: (dnsConfigState.ratelimit_subnet_len_ipv6 ?? IPV6_SUBNET_PREFIX.DEFAULT),
|
||||
{
|
||||
validate: (v) =>
|
||||
validateRequiredValue(String(v)) ||
|
||||
validateBetween(v, range().MIN, range().MAX) ||
|
||||
'',
|
||||
},
|
||||
);
|
||||
|
||||
const storeKey = () => (isV4() ? 'ratelimit_subnet_len_ipv4' : 'ratelimit_subnet_len_ipv6');
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_subnet_prefix_title', {
|
||||
value: isV4() ? IP_VERSION.V4 : IP_VERSION.V6,
|
||||
})}
|
||||
description={intl.getMessage('dns_subnet_prefix_desc', {
|
||||
value: isV4() ? IP_VERSION.V4 : IP_VERSION.V6,
|
||||
})}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
const key = storeKey();
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ [key]: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value()}
|
||||
onChange={(e: Event) =>
|
||||
field.setValue(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() => field.validate()}
|
||||
id={`ratelimit_subnet_len_${isV4() ? 'ipv4' : 'ipv6'}`}
|
||||
label={intl.getMessage('dns_subnet_prefix_title', {
|
||||
value: isV4() ? IP_VERSION.V4 : IP_VERSION.V6,
|
||||
})}
|
||||
placeholder={intl.getMessage('dns_subnet_placeholder')}
|
||||
min={range().MIN}
|
||||
max={range().MAX}
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { IPV4_SUBNET_PREFIX, IP_VERSION } from 'panel/helpers/constants';
|
||||
import { validateRequiredValue, validateBetween } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const SubnetPrefixV4Dialog = (props: Props) => {
|
||||
const field = useField<number>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.ratelimit_subnet_len_ipv4 ?? IPV4_SUBNET_PREFIX.DEFAULT,
|
||||
{
|
||||
validate: (v) =>
|
||||
validateRequiredValue(String(v)) ||
|
||||
validateBetween(v, IPV4_SUBNET_PREFIX.MIN, IPV4_SUBNET_PREFIX.MAX) ||
|
||||
'',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_subnet_prefix_title', { value: IP_VERSION.V4 })}
|
||||
description={intl.getMessage('dns_subnet_prefix_desc', { value: IP_VERSION.V4 })}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ ratelimit_subnet_len_ipv4: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value()}
|
||||
onChange={(e: Event) =>
|
||||
field.setValue(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() => field.validate()}
|
||||
id="ratelimit_subnet_len_ipv4"
|
||||
label={intl.getMessage('dns_subnet_prefix_title', { value: IP_VERSION.V4 })}
|
||||
placeholder={intl.getMessage('dns_subnet_placeholder')}
|
||||
min={IPV4_SUBNET_PREFIX.MIN}
|
||||
max={IPV4_SUBNET_PREFIX.MAX}
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { IPV6_SUBNET_PREFIX, IP_VERSION } from 'panel/helpers/constants';
|
||||
import { validateRequiredValue, validateBetween } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const SubnetPrefixV6Dialog = (props: Props) => {
|
||||
const field = useField<number>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.ratelimit_subnet_len_ipv6 ?? IPV6_SUBNET_PREFIX.DEFAULT,
|
||||
{
|
||||
validate: (v) =>
|
||||
validateRequiredValue(String(v)) ||
|
||||
validateBetween(v, IPV6_SUBNET_PREFIX.MIN, IPV6_SUBNET_PREFIX.MAX) ||
|
||||
'',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_subnet_prefix_title', { value: IP_VERSION.V6 })}
|
||||
description={intl.getMessage('dns_subnet_prefix_desc', { value: IP_VERSION.V6 })}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ ratelimit_subnet_len_ipv6: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value()}
|
||||
onChange={(e: Event) =>
|
||||
field.setValue(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() => field.validate()}
|
||||
id="ratelimit_subnet_len_ipv6"
|
||||
label={intl.getMessage('dns_subnet_prefix_title', { value: IP_VERSION.V6 })}
|
||||
placeholder={intl.getMessage('dns_subnet_placeholder')}
|
||||
min={IPV6_SUBNET_PREFIX.MIN}
|
||||
max={IPV6_SUBNET_PREFIX.MAX}
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,58 +1,47 @@
|
|||
import { createMemo } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import cn from 'clsx';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import { dnsConfigState, togglePrivatePtrResolvers } from 'panel/stores/dnsConfig';
|
||||
import { settingsState, testUpstreamWithFormValues } from 'panel/stores/settings';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import { SettingRow } from 'panel/common/ui/SettingRow';
|
||||
import { useDialogOpen } from 'panel/hooks/useField';
|
||||
import { getUpstreamModeSummary, getUpstreamServersSummary, getTtlSummary } from '../helpers';
|
||||
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;
|
||||
};
|
||||
import { UpstreamModeDialog } from './blocks/UpstreamModeDialog';
|
||||
import { ServerAddressesDialog } from './blocks/ServerAddressesDialog';
|
||||
import { FallbackDnsDialog } from './blocks/FallbackDnsDialog';
|
||||
import { BootstrapDnsDialog } from './blocks/BootstrapDnsDialog';
|
||||
import { TimeoutDialog } from './blocks/TimeoutDialog';
|
||||
import { Paths } from 'panel/components/Routes/Paths';
|
||||
|
||||
export const Upstream = () => {
|
||||
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 navigate = useNavigate();
|
||||
|
||||
const upstreamDnsFile = dnsConfigState.upstream_dns_file;
|
||||
const dnsConfig = {
|
||||
fallback_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
resolve_clients,
|
||||
local_ptr_upstreams,
|
||||
use_private_ptr_resolvers,
|
||||
upstream_timeout,
|
||||
...(upstreamDnsFile ? null : { upstream_dns }),
|
||||
};
|
||||
const upstreamModeDialog = useDialogOpen();
|
||||
const serverAddressesDialog = useDialogOpen();
|
||||
const fallbackDialog = useDialogOpen();
|
||||
const bootstrapDialog = useDialogOpen();
|
||||
const timeoutDialog = useDialogOpen();
|
||||
|
||||
setDnsConfig(dnsConfig);
|
||||
};
|
||||
|
||||
const upstreamDns = createMemo(() =>
|
||||
dnsConfigState.upstream_dns_file
|
||||
? intl.getMessage('upstream_dns_configured_in_file', {
|
||||
path: dnsConfigState.upstream_dns_file,
|
||||
})
|
||||
: dnsConfigState.upstream_dns,
|
||||
const upstreamModeValue = createMemo(() =>
|
||||
getUpstreamModeSummary(dnsConfigState.upstream_mode),
|
||||
);
|
||||
const serverAddressesValue = createMemo(() =>
|
||||
getUpstreamServersSummary(dnsConfigState.upstream_dns, dnsConfigState.upstream_dns_file),
|
||||
);
|
||||
const fallbackServersValue = createMemo(() =>
|
||||
getUpstreamServersSummary(dnsConfigState.fallback_dns),
|
||||
);
|
||||
const bootstrapServersValue = createMemo(() =>
|
||||
getUpstreamServersSummary(dnsConfigState.bootstrap_dns),
|
||||
);
|
||||
const timeoutValue = createMemo(() => getTtlSummary(dnsConfigState.upstream_timeout));
|
||||
|
||||
const processing = () => dnsConfigState.processingSetConfig;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -60,18 +49,113 @@ export const Upstream = () => {
|
|||
{intl.getMessage('upstream_dns')}
|
||||
</h2>
|
||||
|
||||
<Form
|
||||
initialValues={{
|
||||
upstream_dns: upstreamDns(),
|
||||
fallback_dns: dnsConfigState.fallback_dns,
|
||||
bootstrap_dns: dnsConfigState.bootstrap_dns,
|
||||
upstream_mode: dnsConfigState.upstream_mode,
|
||||
resolve_clients: dnsConfigState.resolve_clients,
|
||||
local_ptr_upstreams: dnsConfigState.local_ptr_upstreams,
|
||||
use_private_ptr_resolvers: dnsConfigState.use_private_ptr_resolvers,
|
||||
upstream_timeout: dnsConfigState.upstream_timeout,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="upstream_mode"
|
||||
title={intl.getMessage('dns_upstream_mode')}
|
||||
description={intl.getMessage('dns_upstream_mode_desc')}
|
||||
value={upstreamModeValue()}
|
||||
onClick={upstreamModeDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="server_addresses"
|
||||
title={intl.getMessage('dns_server_addresses')}
|
||||
description={intl.getMessage('dns_server_addresses_desc')}
|
||||
value={serverAddressesValue()}
|
||||
onClick={serverAddressesDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="fallback_servers"
|
||||
title={intl.getMessage('dns_fallback_servers')}
|
||||
description={intl.getMessage('dns_fallback_dns_desc')}
|
||||
value={fallbackServersValue()}
|
||||
onClick={fallbackDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="bootstrap_servers"
|
||||
title={intl.getMessage('dns_bootstrap_servers')}
|
||||
description={intl.getMessage('dns_bootstrap_servers_desc')}
|
||||
value={bootstrapServersValue()}
|
||||
onClick={bootstrapDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="link"
|
||||
id="upstream_timeout"
|
||||
title={intl.getMessage('dns_upstream_timeout')}
|
||||
description={intl.getMessage('dns_upstream_timeout_desc')}
|
||||
value={timeoutValue()}
|
||||
onClick={timeoutDialog.openDialog}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
variant="switch-link"
|
||||
id="private_reverse"
|
||||
title={intl.getMessage('dns_private_reverse_resolvers')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_private_reverse_resolvers_desc')}</p>
|
||||
<p>{intl.getMessage('dns_private_reverse_resolvers_disabled_desc')}</p>
|
||||
</>
|
||||
}
|
||||
checked={dnsConfigState.use_private_ptr_resolvers}
|
||||
onChange={() => togglePrivatePtrResolvers()}
|
||||
onClick={() => navigate(Paths.DnsPrivateReverse)}
|
||||
divider
|
||||
/>
|
||||
|
||||
<div class={theme.form.actionRow}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={settingsState.processingTestUpstream}
|
||||
onClick={() =>
|
||||
testUpstreamWithFormValues(
|
||||
{
|
||||
bootstrap_dns: dnsConfigState.bootstrap_dns,
|
||||
upstream_dns: dnsConfigState.upstream_dns,
|
||||
local_ptr_upstreams: dnsConfigState.local_ptr_upstreams,
|
||||
fallback_dns: dnsConfigState.fallback_dns,
|
||||
},
|
||||
dnsConfigState.upstream_dns_file,
|
||||
)
|
||||
}
|
||||
class={theme.form.actionButton}
|
||||
compact
|
||||
>
|
||||
{intl.getMessage('dns_test_upstreams')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<UpstreamModeDialog
|
||||
open={upstreamModeDialog.open}
|
||||
onClose={upstreamModeDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
<ServerAddressesDialog
|
||||
open={serverAddressesDialog.open}
|
||||
onClose={serverAddressesDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
<FallbackDnsDialog
|
||||
open={fallbackDialog.open}
|
||||
onClose={fallbackDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
<BootstrapDnsDialog
|
||||
open={bootstrapDialog.open}
|
||||
onClose={bootstrapDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
<TimeoutDialog
|
||||
open={timeoutDialog.open}
|
||||
onClose={timeoutDialog.closeDialog}
|
||||
processing={processing()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateUpstreams } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import { Examples } from './Examples';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const BootstrapDnsDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.bootstrap_dns,
|
||||
{ validate: (v) => (v ? validateUpstreams(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_bootstrap_dns_title')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_bootstrap_dns_desc')}</p>
|
||||
<p>{intl.getMessage('dns_bootstrap_dns_desc_2')}</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ bootstrap_dns: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id="bootstrap_dns"
|
||||
label={intl.getMessage('dns_bootstrap_dns_label')}
|
||||
placeholder={intl.getMessage('dns_bootstrap_dns_placeholder')}
|
||||
errorMessage={field.error()}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
<Examples />
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -28,13 +28,13 @@
|
|||
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 8px 16px 8px 44px;
|
||||
padding: 8px 16px 8px 24px;
|
||||
line-height: var(--lh-t1-normal);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 16px;
|
||||
left: 0;
|
||||
color: var(--default-black-icons);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,140 +9,142 @@ 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 = () => {
|
||||
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 class={s.list}>
|
||||
<For each={examples}>
|
||||
{(example) => (
|
||||
<div class={cn(theme.text.t3, s.listItem)}>
|
||||
<Icon icon="label" class={s.icon} />
|
||||
return (
|
||||
<Accordion title={intl.getMessage('upstream_examples_title')} compact>
|
||||
<div class={s.list}>
|
||||
<For each={examples}>
|
||||
{(example) => (
|
||||
<div class={cn(theme.text.t3, s.listItem)}>
|
||||
<Icon icon="label" class={s.icon} />
|
||||
|
||||
{example}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Accordion>
|
||||
);
|
||||
{example}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateUpstreams } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import { Examples } from './Examples';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const FallbackDnsDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.fallback_dns,
|
||||
{ validate: (v) => (v ? validateUpstreams(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_fallback_dns_title')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_fallback_dns_desc')}</p>
|
||||
<p>
|
||||
{intl.getMessage('dns_fallback_dns_desc_2', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={theme.link.link}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ fallback_dns: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id="fallback_dns"
|
||||
label={intl.getMessage('dns_fallback_dns_label')}
|
||||
placeholder={intl.getMessage('dns_fallback_dns_placeholder')}
|
||||
errorMessage={field.error()}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
<Examples />
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,418 +0,0 @@
|
|||
import { createSignal, createMemo } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { settingsState, testUpstreamWithFormValues } from 'panel/stores/settings';
|
||||
import { dnsConfigState } from 'panel/stores/dnsConfig';
|
||||
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 intl from 'panel/common/intl';
|
||||
import { DNS_REQUEST_OPTIONS, UINT32_RANGE, UPSTREAM_TIMEOUT } from 'panel/helpers/constants';
|
||||
import { validateMinValue, validateMaxValue, validateUpstreams } from 'panel/helpers/validators';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const Form = (props: FormProps) => {
|
||||
const [upstreamDns, setUpstreamDns] = createSignal(props.initialValues?.upstream_dns || '');
|
||||
const [upstreamMode, setUpstreamMode] = createSignal(
|
||||
props.initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||
);
|
||||
const [fallbackDns, setFallbackDns] = createSignal(props.initialValues?.fallback_dns || '');
|
||||
const [bootstrapDns, setBootstrapDns] = createSignal(props.initialValues?.bootstrap_dns || '');
|
||||
const [localPtrUpstreams, setLocalPtrUpstreams] = createSignal(
|
||||
props.initialValues?.local_ptr_upstreams || '',
|
||||
);
|
||||
const [usePrivatePtrResolvers, setUsePrivatePtrResolvers] = createSignal(
|
||||
props.initialValues?.use_private_ptr_resolvers || false,
|
||||
);
|
||||
const [resolveClients, setResolveClients] = createSignal(
|
||||
props.initialValues?.resolve_clients || false,
|
||||
);
|
||||
const [upstreamTimeout, setUpstreamTimeout] = createSignal(
|
||||
props.initialValues?.upstream_timeout || 0,
|
||||
);
|
||||
|
||||
const [upstreamDnsError, setUpstreamDnsError] = createSignal('');
|
||||
const [fallbackDnsError, setFallbackDnsError] = createSignal('');
|
||||
const [bootstrapDnsError, setBootstrapDnsError] = createSignal('');
|
||||
const [localPtrError, setLocalPtrError] = createSignal('');
|
||||
const [upstreamTimeoutError, setUpstreamTimeoutError] = createSignal('');
|
||||
|
||||
const upstreamModeOptions = createMemo(() => [
|
||||
{
|
||||
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 class={cn(theme.text.t2, s.warning)}>
|
||||
{intl.getMessage('upstream_dns_fastest_addr_warning')}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const handleUpstreamTest = () => {
|
||||
testUpstreamWithFormValues(
|
||||
{
|
||||
bootstrap_dns: bootstrapDns(),
|
||||
upstream_dns: upstreamDns(),
|
||||
local_ptr_upstreams: localPtrUpstreams(),
|
||||
fallback_dns: fallbackDns(),
|
||||
},
|
||||
dnsConfigState.upstream_dns_file,
|
||||
);
|
||||
};
|
||||
|
||||
const isSavingDisabled = createMemo(
|
||||
() => settingsState.processingTestUpstream || dnsConfigState.processingSetConfig,
|
||||
);
|
||||
|
||||
const isTestDisabled = createMemo(() => !upstreamDns() || settingsState.processingTestUpstream);
|
||||
|
||||
const validateUpstreamDns = () => {
|
||||
const err = upstreamDns() ? validateUpstreams(upstreamDns()) : undefined;
|
||||
setUpstreamDnsError(err || '');
|
||||
};
|
||||
|
||||
const validateFallbackDns = () => {
|
||||
const err = fallbackDns() ? validateUpstreams(fallbackDns()) : undefined;
|
||||
setFallbackDnsError(err || '');
|
||||
};
|
||||
|
||||
const validateBootstrapDns = () => {
|
||||
const err = bootstrapDns() ? validateUpstreams(bootstrapDns()) : undefined;
|
||||
setBootstrapDnsError(err || '');
|
||||
};
|
||||
|
||||
const validateLocalPtr = () => {
|
||||
const err = localPtrUpstreams() ? validateUpstreams(localPtrUpstreams()) : undefined;
|
||||
setLocalPtrError(err || '');
|
||||
};
|
||||
|
||||
const validateUpstreamTimeout = () => {
|
||||
const err =
|
||||
validateMinValue(upstreamTimeout(), UPSTREAM_TIMEOUT.MIN) ||
|
||||
validateMaxValue(upstreamTimeout(), UPSTREAM_TIMEOUT.MAX);
|
||||
setUpstreamTimeoutError(err || '');
|
||||
};
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
validateUpstreamDns();
|
||||
validateFallbackDns();
|
||||
validateBootstrapDns();
|
||||
validateLocalPtr();
|
||||
validateUpstreamTimeout();
|
||||
|
||||
if (
|
||||
upstreamDnsError() ||
|
||||
fallbackDnsError() ||
|
||||
bootstrapDnsError() ||
|
||||
localPtrError() ||
|
||||
upstreamTimeoutError()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onSubmit({
|
||||
upstream_dns: upstreamDns(),
|
||||
upstream_mode: upstreamMode(),
|
||||
fallback_dns: fallbackDns(),
|
||||
bootstrap_dns: bootstrapDns(),
|
||||
local_ptr_upstreams: localPtrUpstreams(),
|
||||
use_private_ptr_resolvers: usePrivatePtrResolvers(),
|
||||
resolve_clients: resolveClients(),
|
||||
upstream_timeout: upstreamTimeout(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class={theme.form.group}>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={upstreamDns()}
|
||||
onChange={(e: Event) =>
|
||||
setUpstreamDns((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={validateUpstreamDns}
|
||||
id="upstream_dns"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('upstream_dns_addresses')}
|
||||
<FaqTooltip
|
||||
text={intl.getMessage('upstream_dns_addresses_faq', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={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"
|
||||
class={theme.link.link}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={intl.getMessage('upstream_dns_placeholder')}
|
||||
disabled={
|
||||
!!dnsConfigState.upstream_dns_file ||
|
||||
settingsState.processingTestUpstream
|
||||
}
|
||||
size="medium"
|
||||
errorMessage={upstreamDnsError()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Radio
|
||||
name="upstream_mode"
|
||||
value={upstreamMode()}
|
||||
handleChange={(v: string) => setUpstreamMode(v)}
|
||||
options={upstreamModeOptions()}
|
||||
disabled={settingsState.processingTestUpstream}
|
||||
verticalAlign="start"
|
||||
textClass={s.radioText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Examples />
|
||||
|
||||
<div class={theme.form.group}>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={fallbackDns()}
|
||||
onChange={(e: Event) =>
|
||||
setFallbackDns((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={validateFallbackDns}
|
||||
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')}
|
||||
size="medium"
|
||||
errorMessage={fallbackDnsError()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={bootstrapDns()}
|
||||
onChange={(e: Event) =>
|
||||
setBootstrapDns((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={validateBootstrapDns}
|
||||
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')}
|
||||
size="medium"
|
||||
errorMessage={bootstrapDnsError()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={localPtrUpstreams()}
|
||||
onChange={(e: Event) =>
|
||||
setLocalPtrUpstreams((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={validateLocalPtr}
|
||||
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>
|
||||
{dnsConfigState.default_local_ptr_upstreams?.length >
|
||||
0 && (
|
||||
<div>
|
||||
{intl.getMessage('upstream_ptr_faq_3', {
|
||||
value_1:
|
||||
dnsConfigState
|
||||
.default_local_ptr_upstreams[0],
|
||||
value_2:
|
||||
dnsConfigState
|
||||
.default_local_ptr_upstreams[1],
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
menuSize="large"
|
||||
spacing
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={intl.getMessage('ip_addresses_placeholder')}
|
||||
size="medium"
|
||||
errorMessage={localPtrError()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
id="dns_use_private_ptr_resolvers"
|
||||
name="use_private_ptr_resolvers"
|
||||
checked={usePrivatePtrResolvers()}
|
||||
onChange={(e: Event) =>
|
||||
setUsePrivatePtrResolvers((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>
|
||||
{intl.getMessage('upstream_private_ptr_resolvers_title')}
|
||||
</div>
|
||||
<div class={theme.text.t4}>
|
||||
{intl.getMessage('upstream_private_ptr_resolvers_desc')}
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Checkbox
|
||||
id="dns_resolve_clients"
|
||||
name="resolve_clients"
|
||||
checked={resolveClients()}
|
||||
onChange={(e: Event) =>
|
||||
setResolveClients((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
verticalAlign="start"
|
||||
>
|
||||
<div>
|
||||
<div class={theme.text.t2}>
|
||||
{intl.getMessage('upstream_enable_reverse_lookup_title')}
|
||||
</div>
|
||||
<div class={theme.text.t4}>
|
||||
{intl.getMessage('upstream_enable_reverse_lookup_desc')}
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
value={String(upstreamTimeout())}
|
||||
onChange={(e: Event) =>
|
||||
setUpstreamTimeout(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={validateUpstreamTimeout}
|
||||
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')}
|
||||
errorMessage={upstreamTimeoutError()}
|
||||
min={UPSTREAM_TIMEOUT.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={theme.form.buttonGroup}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
id="dns_upstream_save"
|
||||
disabled={isSavingDisabled()}
|
||||
class={theme.form.button}
|
||||
>
|
||||
{intl.getMessage('apply')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
id="dns_upstream_test"
|
||||
onClick={handleUpstreamTest}
|
||||
disabled={isTestDisabled()}
|
||||
class={theme.form.button}
|
||||
>
|
||||
{intl.getMessage('test_upstreams')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Form } from './Form';
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateUpstreams } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import { Examples } from './Examples';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const PrivateReverseServersDialog = (props: Props) => {
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.local_ptr_upstreams,
|
||||
{ validate: (v) => (v ? validateUpstreams(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_private_reverse_servers_title')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_private_reverse_servers_desc')}</p>
|
||||
{dnsConfigState.default_local_ptr_upstreams.length >= 2 && (
|
||||
<p>
|
||||
{intl.getMessage('dns_private_reverse_servers_desc_2', {
|
||||
value_1: dnsConfigState.default_local_ptr_upstreams[0],
|
||||
value_2: dnsConfigState.default_local_ptr_upstreams[1],
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ local_ptr_upstreams: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) => field.setValue((e.target as HTMLTextAreaElement).value)}
|
||||
onBlur={() => field.validate()}
|
||||
id="local_ptr_upstreams"
|
||||
label={intl.getMessage('dns_private_reverse_servers_label')}
|
||||
placeholder={intl.getMessage('dns_private_reverse_servers_placeholder')}
|
||||
errorMessage={field.error()}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
<Examples />
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { untrack, type Accessor, Show } from 'solid-js';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import { validateUpstreams } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import { Examples } from './Examples';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const ServerAddressesDialog = (props: Props) => {
|
||||
const hasFile = () => !!dnsConfigState.upstream_dns_file;
|
||||
|
||||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.upstream_dns,
|
||||
{ validate: (v) => (v ? validateUpstreams(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_server_addresses_title')}
|
||||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_server_addresses_desc')}</p>
|
||||
<p>
|
||||
{intl.getMessage('dns_server_addresses_desc_2', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={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"
|
||||
class={theme.link.link}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
if (hasFile()) {
|
||||
props.onClose();
|
||||
return;
|
||||
}
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ upstream_dns: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
hideSubmit={hasFile()}
|
||||
footer={
|
||||
hasFile() ? (
|
||||
<Button variant="primary" onClick={props.onClose}>
|
||||
{intl.getMessage('close')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={!hasFile()}
|
||||
fallback={
|
||||
<p>
|
||||
{intl.getMessage('dns_server_addresses_configured_in_file', {
|
||||
path: dnsConfigState.upstream_dns_file,
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Textarea
|
||||
value={field.value()}
|
||||
onChange={(e: Event) =>
|
||||
field.setValue((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
onBlur={() => field.validate()}
|
||||
id="upstream_dns"
|
||||
label={intl.getMessage('dns_server_addresses_label')}
|
||||
placeholder={intl.getMessage('dns_server_addresses_placeholder')}
|
||||
errorMessage={field.error()}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
<Examples />
|
||||
</Show>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { UPSTREAM_TIMEOUT } from 'panel/helpers/constants';
|
||||
import { validateMinValue, validateMaxValue } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const TimeoutDialog = (props: Props) => {
|
||||
const field = useField<number>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.upstream_timeout,
|
||||
{
|
||||
validate: (v) =>
|
||||
validateMinValue(v, UPSTREAM_TIMEOUT.MIN) ||
|
||||
validateMaxValue(v, UPSTREAM_TIMEOUT.MAX) ||
|
||||
'',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_upstream_timeout_title')}
|
||||
description={intl.getMessage('dns_upstream_timeout_desc')}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
field.submitIfValid((v) => {
|
||||
setDnsConfig({ upstream_timeout: v });
|
||||
untrack(() => props.onClose());
|
||||
});
|
||||
}}
|
||||
processing={props.processing}
|
||||
submitDisabled={!!field.error()}
|
||||
>
|
||||
<div class={theme.form.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value()}
|
||||
onChange={(e: Event) =>
|
||||
field.setValue(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
onBlur={() => field.validate()}
|
||||
id="upstream_timeout"
|
||||
label={intl.getMessage('upstream_timeout')}
|
||||
placeholder={intl.getMessage('dns_upstream_timeout_placeholder')}
|
||||
min={UPSTREAM_TIMEOUT.MIN}
|
||||
max={UPSTREAM_TIMEOUT.MAX}
|
||||
errorMessage={field.error()}
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
.radioText {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-top: 8px;
|
||||
color: var(--default-attention-link);
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { createSignal, createEffect, createMemo, type Accessor } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import { getUpstreamModeOptions } from 'panel/helpers/constants';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './UpstreamModeDialog.module.pcss';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
onClose: () => void;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
export const UpstreamModeDialog = (props: Props) => {
|
||||
const [upstreamMode, setUpstreamMode] = createSignal(dnsConfigState.upstream_mode);
|
||||
|
||||
createEffect(() => {
|
||||
if (props.open()) {
|
||||
setUpstreamMode(dnsConfigState.upstream_mode);
|
||||
}
|
||||
});
|
||||
|
||||
const upstreamModeOptions = createMemo(() => {
|
||||
return getUpstreamModeOptions().map((opt) => ({
|
||||
text: opt.text,
|
||||
value: opt.value,
|
||||
description: opt.warning ? (
|
||||
<>
|
||||
{opt.description}
|
||||
<div class={cn(theme.text.t3, s.warning)}>{opt.warning}</div>
|
||||
</>
|
||||
) : (
|
||||
opt.description
|
||||
),
|
||||
}));
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigDialog
|
||||
open={props.open()}
|
||||
title={intl.getMessage('dns_upstream_mode_title')}
|
||||
description={intl.getMessage('dns_upstream_mode_desc')}
|
||||
onClose={props.onClose}
|
||||
onSubmit={() => {
|
||||
setDnsConfig({ upstream_mode: upstreamMode() });
|
||||
props.onClose();
|
||||
}}
|
||||
processing={props.processing}
|
||||
>
|
||||
<Radio
|
||||
name="upstream_mode"
|
||||
options={upstreamModeOptions()}
|
||||
value={upstreamMode()}
|
||||
handleChange={(v: string) => setUpstreamMode(v)}
|
||||
inModal
|
||||
/>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
60
client_v3/src/components/DnsSettings/helpers.ts
Normal file
60
client_v3/src/components/DnsSettings/helpers.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { DNS_REQUEST_OPTIONS } from 'panel/helpers/constants';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
export const getUpstreamModeSummary = (mode: string): string => {
|
||||
switch (mode) {
|
||||
case DNS_REQUEST_OPTIONS.PARALLEL:
|
||||
return intl.getMessage('upstream_dns_parallel_requests');
|
||||
case DNS_REQUEST_OPTIONS.FASTEST_ADDR:
|
||||
return intl.getMessage('upstream_dns_fastest_addr');
|
||||
case DNS_REQUEST_OPTIONS.LOAD_BALANCING:
|
||||
default:
|
||||
return intl.getMessage('upstream_dns_load_balancing');
|
||||
}
|
||||
};
|
||||
|
||||
export const getUpstreamServersSummary = (servers: string, upstreamDnsFile?: string): string => {
|
||||
if (upstreamDnsFile) {
|
||||
return intl.getMessage('upstream_dns_configured_in_file', { path: upstreamDnsFile });
|
||||
}
|
||||
if (!servers) return '';
|
||||
const lines = servers.split('\n').filter(Boolean);
|
||||
return lines.join(', ');
|
||||
};
|
||||
|
||||
export const getRateLimitSummary = (ratelimit: number): string => {
|
||||
if (ratelimit === 0) return intl.getMessage('dns_rate_limit_no_limit');
|
||||
return intl.getMessage('dns_rate_limit_value', { value: ratelimit });
|
||||
};
|
||||
|
||||
export const getBlockingModeSummary = (mode: string): string => {
|
||||
switch (mode) {
|
||||
case 'refused':
|
||||
return 'REFUSED';
|
||||
case 'nxdomain':
|
||||
return 'NXDOMAIN';
|
||||
case 'null_ip':
|
||||
return intl.getMessage('dns_blocking_mode_null_ip');
|
||||
case 'custom_ip':
|
||||
return intl.getMessage('dns_blocking_mode_custom_ip');
|
||||
default:
|
||||
return intl.getMessage('dns_blocking_mode_default');
|
||||
}
|
||||
};
|
||||
|
||||
export const getCacheSizeSummary = (bytes?: number): string => {
|
||||
if (!bytes && bytes !== 0) return '';
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
||||
return `${Number((bytes / (1024 * 1024)).toFixed(1))} MB`;
|
||||
};
|
||||
|
||||
export const getTtlSummary = (seconds?: number): string => {
|
||||
if (seconds === undefined || seconds === null) return '';
|
||||
return intl.getMessage('dns_ttl_value', { value: seconds });
|
||||
};
|
||||
|
||||
export const getListSummary = (list: string): string => {
|
||||
if (!list || !list.trim()) return '';
|
||||
return list.split('\n').filter(Boolean).join(', ');
|
||||
};
|
||||
|
|
@ -50,12 +50,13 @@ type Props = {
|
|||
export const FiltersList = (props: Props) => {
|
||||
const { categories, filters } = filtersCatalog;
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- curried handler passed to JSX onChange (tracked)
|
||||
const handleToggle = (filterId: string) => (e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
untrack(() => props).onChange({
|
||||
...props.selectedIds,
|
||||
[filterId]: checked,
|
||||
untrack(() => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
props.onChange({
|
||||
...props.selectedIds,
|
||||
[filterId]: checked,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -47,17 +47,17 @@ export const ListsTable = (props: Props) => {
|
|||
const sortedFilters = createMemo(() => {
|
||||
const items = [...(props.filters || [])];
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- sort callback inside createMemo; reads already tracked
|
||||
const direction = sortDirection();
|
||||
items.sort((a, b) => {
|
||||
const aName = (a.name || '').toLowerCase();
|
||||
const bName = (b.name || '').toLowerCase();
|
||||
|
||||
if (aName < bName) {
|
||||
return sortDirection() === 'asc' ? -1 : 1;
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
|
||||
if (aName > bName) {
|
||||
return sortDirection() === 'asc' ? 1 : -1;
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -37,17 +37,17 @@ export const RewritesTable = (props: Props) => {
|
|||
const sortedList = createMemo(() => {
|
||||
const items = [...(props.list || [])];
|
||||
|
||||
// eslint-disable-next-line solid/reactivity -- sort callback inside createMemo; reads already tracked
|
||||
const direction = sortDirection();
|
||||
items.sort((a, b) => {
|
||||
const aDomain = a.domain.toLowerCase();
|
||||
const bDomain = b.domain.toLowerCase();
|
||||
|
||||
if (aDomain < bDomain) {
|
||||
return sortDirection() === 'asc' ? -1 : 1;
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
|
||||
if (aDomain > bDomain) {
|
||||
return sortDirection() === 'asc' ? 1 : -1;
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createMemo, Show } from 'solid-js';
|
||||
import { createMemo, Show, untrack } from 'solid-js';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { Loader } from 'panel/common/ui/Loader';
|
||||
|
|
@ -40,12 +40,10 @@ type Props = {
|
|||
};
|
||||
|
||||
export const LogTable = (props: Props) => {
|
||||
/* eslint-disable solid/reactivity -- curried handler, called from JSX (tracked) */
|
||||
const handleSearchSelect = (value: string) => (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
props.onSearchSelect(value);
|
||||
untrack(() => props.onSearchSelect(value));
|
||||
};
|
||||
/* eslint-enable solid/reactivity */
|
||||
|
||||
const columns = createMemo<TableColumn<LogEntry>[]>(() => [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const RoutePath = {
|
|||
Encryption: 'Encryption',
|
||||
Dhcp: 'Dhcp',
|
||||
Dns: 'Dns',
|
||||
DnsPrivateReverse: 'DnsPrivateReverse',
|
||||
SettingsPage: 'SettingsPage',
|
||||
Clients: 'Clients',
|
||||
DnsBlocklists: 'DnsBlocklists',
|
||||
|
|
@ -41,6 +42,7 @@ export const Paths: Record<RoutePathKey, string> = {
|
|||
Encryption: pathBuilder('encryption'),
|
||||
Dhcp: pathBuilder('dhcp'),
|
||||
Dns: pathBuilder('dns'),
|
||||
DnsPrivateReverse: pathBuilder('dns/private-reverse'),
|
||||
SettingsPage: pathBuilder('settings'),
|
||||
Clients: pathBuilder('clients'),
|
||||
DnsBlocklists: pathBuilder('blocklists'),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import intl from 'panel/common/intl';
|
||||
|
||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
|
||||
|
||||
// matches hostname or *.wildcard
|
||||
|
|
@ -220,6 +222,11 @@ export const BLOCKING_MODES = {
|
|||
custom_ip: 'custom_ip',
|
||||
};
|
||||
|
||||
export const EDNS_MODES = {
|
||||
default: 'default',
|
||||
custom: 'custom',
|
||||
};
|
||||
|
||||
// Note that translation strings contain these modes (theme_CONSTANT)
|
||||
// i.e. theme_auto, theme_light.
|
||||
export const THEMES = {
|
||||
|
|
@ -506,13 +513,20 @@ export const RATE_LIMIT = {
|
|||
export const IPV4_SUBNET_PREFIX = {
|
||||
MIN: 0,
|
||||
MAX: 32,
|
||||
DEFAULT: 24,
|
||||
};
|
||||
|
||||
export const IPV6_SUBNET_PREFIX = {
|
||||
MIN: 0,
|
||||
MAX: 128,
|
||||
DEFAULT: 56,
|
||||
};
|
||||
|
||||
export const IP_VERSION = {
|
||||
V4: 'IPv4',
|
||||
V6: 'IPv6',
|
||||
} as const;
|
||||
|
||||
export const UPSTREAM_TIMEOUT = {
|
||||
MIN: 1,
|
||||
MAX: UINT32_RANGE.MAX,
|
||||
|
|
@ -553,6 +567,84 @@ export const TOAST_TYPES = {
|
|||
NOTICE: 'notice',
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns blocking mode options with i18n text.
|
||||
* Factory function — re-executes on each render for locale correctness.
|
||||
*/
|
||||
export const getBlockingModeOptions = () => {
|
||||
return [
|
||||
{
|
||||
text: intl.getMessage('dns_blocking_mode_default'),
|
||||
value: BLOCKING_MODES.default,
|
||||
description: intl.getMessage('dns_blocking_mode_default_desc'),
|
||||
},
|
||||
{
|
||||
text: intl.getMessage('dns_blocking_mode_refused'),
|
||||
value: BLOCKING_MODES.refused,
|
||||
description: intl.getMessage('dns_blocking_mode_refused_desc'),
|
||||
},
|
||||
{
|
||||
text: intl.getMessage('dns_blocking_mode_nxdomain'),
|
||||
value: BLOCKING_MODES.nxdomain,
|
||||
description: intl.getMessage('dns_blocking_mode_nxdomain_desc'),
|
||||
},
|
||||
{
|
||||
text: intl.getMessage('dns_blocking_mode_null_ip'),
|
||||
value: BLOCKING_MODES.null_ip,
|
||||
description: intl.getMessage('dns_blocking_mode_null_ip_desc'),
|
||||
},
|
||||
{
|
||||
text: intl.getMessage('dns_blocking_mode_custom_ip'),
|
||||
value: BLOCKING_MODES.custom_ip,
|
||||
description: intl.getMessage('dns_blocking_mode_custom_ip_desc'),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns EDNS mode options with i18n text.
|
||||
* Factory function — re-executes on each render for locale correctness.
|
||||
*/
|
||||
export const getEdnsOptions = () => {
|
||||
return [
|
||||
{
|
||||
text: intl.getMessage('dns_edns_option_default'),
|
||||
value: EDNS_MODES.default,
|
||||
},
|
||||
{
|
||||
text: intl.getMessage('dns_edns_option_custom'),
|
||||
value: EDNS_MODES.custom,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns upstream mode options with i18n text.
|
||||
* Factory function — re-executes on each call for locale correctness.
|
||||
* When used inside `createMemo`, tracks the locale signal and updates
|
||||
* option labels on language change.
|
||||
*/
|
||||
export const getUpstreamModeOptions = () => {
|
||||
return [
|
||||
{
|
||||
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'),
|
||||
warning: intl.getMessage('upstream_dns_fastest_addr_warning'),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
||||
|
||||
export const ONE_SECOND_IN_MS = 1000;
|
||||
|
|
|
|||
116
client_v3/src/hooks/useField.ts
Normal file
116
client_v3/src/hooks/useField.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { createSignal, createEffect, type Accessor, type Setter } from 'solid-js';
|
||||
|
||||
/**
|
||||
* Configuration for useField hook.
|
||||
*/
|
||||
type UseFieldConfig<T> = {
|
||||
/**
|
||||
* Optional validator function.
|
||||
* Should return an error message string on failure, or empty string on success.
|
||||
*/
|
||||
validate?: (value: T) => string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return type of the useField hook.
|
||||
*/
|
||||
type UseFieldResult<T> = {
|
||||
/** Current field value (reactive accessor). */
|
||||
value: Accessor<T>;
|
||||
/** Update the field value. */
|
||||
setValue: Setter<T>;
|
||||
/** Current validation error message (empty string when valid). */
|
||||
error: Accessor<string>;
|
||||
/** Manually override the error message. */
|
||||
setError: Setter<string>;
|
||||
/**
|
||||
* Run the validator, set the error signal, and return the error message.
|
||||
* Returns empty string if valid.
|
||||
*/
|
||||
validate: () => string;
|
||||
/**
|
||||
* Validate then submit. Runs the validator; if no error, calls onSubmit
|
||||
* with the current value. Returns true if submitted, false if validation failed.
|
||||
*/
|
||||
submitIfValid: (onSubmit: (value: T) => void) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Composable hook that encapsulates the 4-signal + sync-effect pattern
|
||||
* used by DNS settings dialogs.
|
||||
*
|
||||
* When `isOpen` returns true, the field value resets from `getStoreValue()`
|
||||
* and the error is cleared — mirroring the original createEffect behavior.
|
||||
*
|
||||
* @param isOpen Getter that returns whether the dialog is open.
|
||||
* Called inside createEffect for proper reactivity tracking.
|
||||
* @param getStoreValue Lazily reads the store value on each effect re-run.
|
||||
* @param config Optional configuration (validator).
|
||||
*/
|
||||
export function useField<T>(
|
||||
isOpen: () => boolean,
|
||||
getStoreValue: () => T,
|
||||
config?: UseFieldConfig<T>,
|
||||
): UseFieldResult<T> {
|
||||
const [value, setValue] = createSignal<T>(getStoreValue());
|
||||
const [error, setError] = createSignal('');
|
||||
|
||||
// Sync from store when dialog opens — mirrors the original pattern:
|
||||
// createEffect(() => { if (open()) { setValue(storeValue); setError(''); } });
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
setValue(() => getStoreValue());
|
||||
setError('');
|
||||
}
|
||||
});
|
||||
|
||||
const validate = (): string => {
|
||||
const err = config?.validate ? config.validate(value()) : '';
|
||||
setError(err);
|
||||
return err;
|
||||
};
|
||||
|
||||
const submitIfValid = (onSubmit: (v: T) => void): boolean => {
|
||||
const err = validate();
|
||||
if (err) {
|
||||
return false;
|
||||
}
|
||||
onSubmit(value());
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue,
|
||||
error,
|
||||
setError,
|
||||
validate,
|
||||
submitIfValid,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type of the useDialogOpen hook.
|
||||
*/
|
||||
type UseDialogOpenResult = {
|
||||
/** Reactive accessor for the dialog open state. */
|
||||
open: Accessor<boolean>;
|
||||
/** Opens the dialog. */
|
||||
openDialog: () => void;
|
||||
/** Closes the dialog. */
|
||||
closeDialog: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience hook for dialog open/close state.
|
||||
* Returns `open` signal, `openDialog`, and `closeDialog` helpers.
|
||||
*/
|
||||
export function useDialogOpen(): UseDialogOpenResult {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
|
||||
return {
|
||||
open,
|
||||
openDialog: () => setOpen(true),
|
||||
closeDialog: () => setOpen(false),
|
||||
};
|
||||
}
|
||||
|
|
@ -44,3 +44,12 @@
|
|||
font-size: 14px;
|
||||
color: var(--default-error-icon);
|
||||
}
|
||||
|
||||
.actionRow {
|
||||
padding: 8px 16px;
|
||||
|
||||
.actionButton {
|
||||
width: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const setAccessList = async (values: any) => {
|
|||
|
||||
await apiClient.setAccessList(config);
|
||||
setState({ ...values, processingSet: false });
|
||||
addSuccessToast(intl.getMessage('access_settings_saved_toast'));
|
||||
addSuccessToast(intl.getMessage('settings_notify_changes_saved'));
|
||||
} catch (error) {
|
||||
addErrorToast({ error });
|
||||
setState('processingSet', false);
|
||||
|
|
|
|||
|
|
@ -100,7 +100,49 @@ export const clearDnsCache = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setDnsConfig = async (values: any, toastMessage?: string) => {
|
||||
/**
|
||||
* Toggles `use_private_ptr_resolvers`.
|
||||
* Called from the Private Reverse page's header switch.
|
||||
* NOT called from the main DNS settings page.
|
||||
*/
|
||||
export const togglePrivatePtrResolvers = () => {
|
||||
setDnsConfig({ use_private_ptr_resolvers: !state.use_private_ptr_resolvers }, { silent: true });
|
||||
};
|
||||
|
||||
/** Toggles `resolve_clients`. Used by PrivateReverse page switch. */
|
||||
export const toggleResolveClients = () => {
|
||||
setDnsConfig({ resolve_clients: !state.resolve_clients }, { silent: true });
|
||||
};
|
||||
|
||||
/** Toggles `dnssec_enabled`. Used by ServerConfig Section 2, Row 7. */
|
||||
export const toggleDnssecEnabled = () => {
|
||||
setDnsConfig({ dnssec_enabled: !state.dnssec_enabled }, { silent: true });
|
||||
};
|
||||
|
||||
/** Toggles `disable_ipv6` (inverted in UI). Used by ServerConfig Section 2, Row 8. */
|
||||
export const toggleDisableIPv6 = () => {
|
||||
setDnsConfig({ disable_ipv6: !state.disable_ipv6 }, { silent: true });
|
||||
};
|
||||
|
||||
/** Toggles `cache_enabled`. Used by Cache Section 3 header switch. */
|
||||
export const toggleCacheEnabled = () => {
|
||||
setDnsConfig({ cache_enabled: !state.cache_enabled }, { silent: true });
|
||||
};
|
||||
|
||||
/** Toggles `cache_optimistic`. Used by Cache Section 3, Row 4. */
|
||||
export const toggleOptimisticCaching = () => {
|
||||
setDnsConfig({ cache_optimistic: !state.cache_optimistic }, { silent: true });
|
||||
};
|
||||
|
||||
/** Toggles `edns_cs_enabled`. Used by ServerConfig Section 2, Row 6 switch-link. */
|
||||
export const toggleEdnsCsEnabled = () => {
|
||||
setDnsConfig({ edns_cs_enabled: !state.edns_cs_enabled }, { silent: true });
|
||||
};
|
||||
|
||||
export const setDnsConfig = async (
|
||||
values: any,
|
||||
opts?: { toastMessage?: string; silent?: boolean },
|
||||
) => {
|
||||
setState('processingSetConfig', true);
|
||||
try {
|
||||
const config = { ...values };
|
||||
|
|
@ -130,8 +172,10 @@ export const setDnsConfig = async (values: any, toastMessage?: string) => {
|
|||
await apiClient.setDnsConfig(config);
|
||||
setState(reconcile({ ...untrack(() => state), ...values, processingSetConfig: false }));
|
||||
|
||||
if (toastMessage) {
|
||||
addSuccessToast(toastMessage);
|
||||
if (opts?.silent) {
|
||||
// Toggle switches — no toast needed
|
||||
} else if (opts?.toastMessage) {
|
||||
addSuccessToast(opts.toastMessage);
|
||||
} else if (hasDnsSettings) {
|
||||
addSuccessToast(intl.getMessage('updated_upstream_dns_toast'));
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue