mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-10-26 11:27:18 +00:00
ADG-10295 add encryption settings page
This commit is contained in:
parent
3f6d759b7f
commit
cff3f45883
@ -393,40 +393,23 @@
|
||||
"next": "Next",
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"install_saved": "Saved successfully",
|
||||
"encryption_title": "Encryption",
|
||||
"encryption_desc": "Encryption (HTTPS/QUIC/TLS) support for both DNS and admin web interface",
|
||||
"encryption_config_saved": "Encryption configuration saved",
|
||||
"encryption_server": "Server name",
|
||||
"encryption_server_enter": "Enter your domain name",
|
||||
"encryption_server_desc": "If set, AdGuard Home detects ClientIDs, responds to DDR queries, and performs additional connection validations. If not set, these features are disabled. Must match one of the DNS Names in the certificate.",
|
||||
"encryption_redirect": "Redirect to HTTPS automatically",
|
||||
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
|
||||
"encryption_https": "HTTPS port",
|
||||
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.",
|
||||
"encryption_dot": "DNS-over-TLS port",
|
||||
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
|
||||
"encryption_doq": "DNS-over-QUIC port",
|
||||
"encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port.",
|
||||
"encryption_certificates": "Certificates",
|
||||
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
|
||||
"encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.",
|
||||
"encryption_status": "Status",
|
||||
"encryption_expire": "Expires",
|
||||
"encryption_key": "Private key",
|
||||
"encryption_key_input": "Copy/paste your PEM-encoded private key for your certificate here.",
|
||||
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
|
||||
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
|
||||
"encryption_chain_valid": "Certificate chain is valid",
|
||||
"encryption_chain_invalid": "Certificate chain is invalid",
|
||||
"encryption_key_valid": "This is a valid {{type}} private key",
|
||||
"encryption_key_invalid": "This is an invalid {{type}} private key",
|
||||
"encryption_subject": "Subject",
|
||||
"encryption_issuer": "Issuer",
|
||||
"encryption_hostnames": "Hostnames",
|
||||
"encryption_reset": "Are you sure you want to reset encryption settings?",
|
||||
"encryption_warning": "Warning",
|
||||
"encryption_plain_dns_enable": "Enable plain DNS",
|
||||
"encryption_plain_dns_desc": "Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must enable at least one encrypted DNS protocol",
|
||||
"encryption_plain_dns_error": "To disable plain DNS, enable at least one encrypted DNS protocol",
|
||||
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.",
|
||||
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
|
||||
@ -805,5 +788,38 @@
|
||||
"settings_tooltip_examples": "Examples:",
|
||||
"settings_notify_changes_saved": "Changes saved",
|
||||
"settings_notify_query_log_cleared": "Query log cleared",
|
||||
"settings_notify_statistics_cleared": "Statistics cleared"
|
||||
"settings_notify_statistics_cleared": "Statistics cleared",
|
||||
"encryption_title": "Encryption",
|
||||
"encryption_encrypted_dns": "Encrypted DNS",
|
||||
"encryption_encrypted_dns_desc": "The AdGuard Home admin interface uses HTTPS. The DNS server can use DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC",
|
||||
"encryption_plain_dns": "Plain DNS",
|
||||
"encryption_plain_dns_desc": "Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must specify at least one encrypted DNS protocol",
|
||||
"encryption_server": "Server name",
|
||||
"encryption_server_tooltip_1": "If set, AdGuard Home will detect ClientIDs, respond to DDR queries, and perform additional connection validations. If not set, these features are disabled",
|
||||
"encryption_server_tooltip_2": "Must match a DNS name in the certificate",
|
||||
"encryption_https": "HTTPS port",
|
||||
"encryption_https_tooltip": "This port provides HTTPS access for the AdGuard Home admin interface. It also provides DNS-over-HTTPS at the '/dns-query' location",
|
||||
"encryption_dot": "DNS-over-TLS port",
|
||||
"encryption_dot_tooltip": "This port is used to run a DNS-over-TLS server",
|
||||
"encryption_doq": "DNS-over-QUIC port",
|
||||
"encryption_doq_tooltip": "This port is used to run a DNS-over-QUIC server",
|
||||
"encryption_force_redirect": "Automatically redirect from HTTP to HTTPS",
|
||||
"encryption_certificates": "Certificates",
|
||||
"encryption_certificates_desc": "To use encryption, you must provide a valid SSL certificate chain for your domain. You can get a free SSL certificate from <a>letsencrypt.org</a> or buy one from a trusted Certificate Authority",
|
||||
"encryption_key": "Private key",
|
||||
"encryption_disable_plain_dns": "Disable plain DNS?",
|
||||
"encryption_disable_plain_dns_desc": "Devices that do not support encrypted DNS may not work properly",
|
||||
"disable": "Disable",
|
||||
"encryption_chain_valid": "Certificate chain is valid",
|
||||
"encryption_chain_invalid": "Certificate chain is invalid",
|
||||
"encryption_key_valid": "Private key is valid",
|
||||
"encryption_key_invalid": "Private key is invalid",
|
||||
"encryption_subject": "Subject: %value%",
|
||||
"encryption_issuer": "Issuer: %value%",
|
||||
"encryption_expire": "Expires: %value%",
|
||||
"encryption_hostnames": "Hostnames: %value%",
|
||||
"encryption_certificate_has_issues": "Certificate has issues",
|
||||
"encryption_confirm_clear": "Reset encryption settings?",
|
||||
"encryption_confirm_clear_desc": "This will reset all encryption settings to default values",
|
||||
"confirm": "Confirm"
|
||||
}
|
||||
|
||||
@ -36,13 +36,9 @@
|
||||
.label {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: var(--weight-regular);
|
||||
user-select: none;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@ -99,7 +99,9 @@
|
||||
}
|
||||
|
||||
.inputLabel {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
|
||||
@ -3,7 +3,7 @@ import cn from 'clsx';
|
||||
|
||||
import s from './Input.module.pcss';
|
||||
|
||||
type Props = ComponentProps<'input'> & {
|
||||
type Props = Omit<ComponentProps<'input'>, 'size'> & {
|
||||
label?: ReactNode;
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
|
||||
@ -33,5 +33,9 @@
|
||||
}
|
||||
|
||||
.input:disabled + .handler .icon {
|
||||
color: var(--disabled-gray-icons);
|
||||
}
|
||||
|
||||
.input:disabled:checked + .handler .icon {
|
||||
color: var(--disabled-main-button);
|
||||
}
|
||||
|
||||
35
client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx
Normal file
35
client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
type Props = {
|
||||
text: ReactNode;
|
||||
menuSize?: 'small' | 'large';
|
||||
};
|
||||
|
||||
export const FaqTooltip = ({ text, menuSize = 'small' }: Props) => {
|
||||
return (
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
menu={
|
||||
<div
|
||||
className={cn(theme.dropdown.menu, s.menu, {
|
||||
[s.menu_large]: menuSize === 'large',
|
||||
})}>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
className={s.dropdown}
|
||||
position="bottomLeft"
|
||||
noIcon>
|
||||
<div className={s.trigger}>
|
||||
<Icon icon="faq" className={s.icon} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
1
client_v2/src/common/ui/FaqTooltip/index.ts
Normal file
1
client_v2/src/common/ui/FaqTooltip/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { FaqTooltip } from './FaqTooltip';
|
||||
19
client_v2/src/common/ui/FaqTooltip/styles.module.pcss
Normal file
19
client_v2/src/common/ui/FaqTooltip/styles.module.pcss
Normal file
@ -0,0 +1,19 @@
|
||||
.trigger {
|
||||
cursor: pointer;
|
||||
color: var(--default-gray-icons);
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: 16px;
|
||||
background-color: var(--fills-backgrounds-page-background-additional);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
&_large {
|
||||
@media (min-width: 768px) {
|
||||
max-width: 340px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,9 @@ import { Icons } from 'panel/common/ui/Icons';
|
||||
import { Footer } from 'panel/common/ui/Footer';
|
||||
import { Header } from 'panel/common/ui/Header';
|
||||
import { Settings } from 'panel/components/Settings';
|
||||
|
||||
import { LocalesType } from 'panel/common/intl';
|
||||
import { Encryption } from 'panel/components/Encryption';
|
||||
|
||||
import Toasts from '../Toasts';
|
||||
import i18n from '../../i18n';
|
||||
import { THEMES } from '../../helpers/constants';
|
||||
@ -31,6 +32,11 @@ const ROUTES: RouteConfig[] = [
|
||||
component: Settings,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/encryption',
|
||||
component: Encryption,
|
||||
exact: true,
|
||||
},
|
||||
];
|
||||
|
||||
const App = () => {
|
||||
|
||||
564
client_v2/src/components/Encryption/Form.tsx
Normal file
564
client_v2/src/components/Encryption/Form.tsx
Normal file
@ -0,0 +1,564 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import i18next from 'i18next';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { toNumber } from 'panel/helpers/form';
|
||||
import { DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, STANDARD_HTTPS_PORT, ENCRYPTION_SOURCE } from 'panel/helpers/constants';
|
||||
import { EncryptionData } from 'panel/initialState';
|
||||
import {
|
||||
validateServerName,
|
||||
validateIsSafePort,
|
||||
validatePort,
|
||||
validatePortQuic,
|
||||
validatePortTLS,
|
||||
validatePlainDns,
|
||||
} from 'panel/helpers/validators';
|
||||
import { Checkbox } from 'panel/common/controls/Checkbox';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setTlsConfig, validateTlsConfig } from 'panel/actions/encryption';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import intl from 'panel/common/intl';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
|
||||
import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import { KeyStatus, CertificateStatus, ValidationStatus } from './Status';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
const certificateSourceOptions = [
|
||||
{
|
||||
text: i18next.t('encryption_certificates_source_path'),
|
||||
value: ENCRYPTION_SOURCE.PATH,
|
||||
},
|
||||
{
|
||||
text: i18next.t('encryption_certificates_source_content'),
|
||||
value: ENCRYPTION_SOURCE.CONTENT,
|
||||
},
|
||||
];
|
||||
|
||||
const keySourceOptions = [
|
||||
{
|
||||
text: i18next.t('encryption_key_source_path'),
|
||||
value: ENCRYPTION_SOURCE.PATH,
|
||||
},
|
||||
{
|
||||
text: i18next.t('encryption_key_source_content'),
|
||||
value: ENCRYPTION_SOURCE.CONTENT,
|
||||
},
|
||||
];
|
||||
|
||||
export type EncryptionFormValues = {
|
||||
enabled?: boolean;
|
||||
serve_plain_dns?: boolean;
|
||||
server_name?: string;
|
||||
force_https?: boolean;
|
||||
port_https?: number;
|
||||
port_dns_over_tls?: number;
|
||||
port_dns_over_quic?: number;
|
||||
certificate_chain?: string;
|
||||
private_key?: string;
|
||||
certificate_path?: string;
|
||||
private_key_path?: string;
|
||||
certificate_source?: string;
|
||||
key_source?: string;
|
||||
private_key_saved?: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialValues: EncryptionFormValues;
|
||||
encryption: EncryptionData;
|
||||
onSubmit: (values: EncryptionFormValues) => void;
|
||||
debouncedConfigValidation: (values: EncryptionFormValues) => void;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
enabled: false,
|
||||
serve_plain_dns: true,
|
||||
server_name: '',
|
||||
force_https: false,
|
||||
port_https: STANDARD_HTTPS_PORT,
|
||||
port_dns_over_tls: DNS_OVER_TLS_PORT,
|
||||
port_dns_over_quic: DNS_OVER_QUIC_PORT,
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
certificate_path: '',
|
||||
private_key_path: '',
|
||||
certificate_source: ENCRYPTION_SOURCE.PATH,
|
||||
key_source: ENCRYPTION_SOURCE.PATH,
|
||||
private_key_saved: false,
|
||||
};
|
||||
|
||||
export const Form = ({ initialValues, encryption, onSubmit, debouncedConfigValidation }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
|
||||
|
||||
const {
|
||||
not_after,
|
||||
valid_chain,
|
||||
valid_key,
|
||||
valid_cert,
|
||||
valid_pair,
|
||||
dns_names,
|
||||
issuer,
|
||||
subject,
|
||||
warning_validation,
|
||||
processingConfig,
|
||||
processingValidate,
|
||||
} = encryption;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
reset,
|
||||
setValue,
|
||||
setError,
|
||||
getValues,
|
||||
formState: { isSubmitting, isValid },
|
||||
} = useForm<EncryptionFormValues>({
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
...initialValues,
|
||||
},
|
||||
mode: 'onBlur',
|
||||
});
|
||||
|
||||
const {
|
||||
enabled: isEnabled,
|
||||
serve_plain_dns: servePlainDns,
|
||||
certificate_chain: certificateChain,
|
||||
private_key: privateKey,
|
||||
private_key_path: privateKeyPath,
|
||||
key_source: privateKeySource,
|
||||
private_key_saved: privateKeySaved,
|
||||
certificate_path: certificatePath,
|
||||
certificate_source: certificateSource,
|
||||
} = watch();
|
||||
|
||||
const handleBlur = () => {
|
||||
debouncedConfigValidation(getValues());
|
||||
};
|
||||
|
||||
const isSavingDisabled = () => {
|
||||
const processing = isSubmitting || processingConfig || processingValidate;
|
||||
|
||||
if (servePlainDns && !isEnabled) {
|
||||
return !isValid || processing;
|
||||
}
|
||||
|
||||
return !isValid || processing || !valid_key || !valid_cert || !valid_pair;
|
||||
};
|
||||
|
||||
const handleResetConfirmOpen = () => setOpenConfirmDialog(true);
|
||||
|
||||
const handleResetConfirmClose = () => setOpenConfirmDialog(false);
|
||||
|
||||
const handleReset = () => {
|
||||
reset();
|
||||
dispatch(setTlsConfig(defaultValues));
|
||||
dispatch(validateTlsConfig(defaultValues));
|
||||
};
|
||||
|
||||
const validatePorts = (values: EncryptionFormValues) => {
|
||||
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
||||
|
||||
if (values.port_dns_over_tls && values.port_https) {
|
||||
if (values.port_dns_over_tls === values.port_https) {
|
||||
errors.port_dns_over_tls = i18next.t('form_error_equal');
|
||||
errors.port_https = i18next.t('form_error_equal');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const onFormSubmit = (data: EncryptionFormValues) => {
|
||||
const validationErrors = validatePorts(data);
|
||||
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
Object.entries(validationErrors).forEach(([field, message]) => {
|
||||
setError(field as keyof EncryptionFormValues, { type: 'manual', message });
|
||||
});
|
||||
} else {
|
||||
onSubmit(data);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = isSavingDisabled();
|
||||
const isWarning = valid_key && valid_cert && valid_pair;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SwitchGroup
|
||||
id="enabled"
|
||||
title={intl.getMessage('encryption_encrypted_dns')}
|
||||
description={intl.getMessage('encryption_encrypted_dns_desc')}
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="serve_plain_dns"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => validatePlainDns(value, getValues()),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<SwitchGroup
|
||||
id="serve_plain_dns"
|
||||
title={intl.getMessage('encryption_plain_dns')}
|
||||
description={intl.getMessage('encryption_plain_dns_desc')}
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={theme.form.group}>
|
||||
<div className={theme.form.input}>
|
||||
<Controller
|
||||
name="server_name"
|
||||
control={control}
|
||||
rules={{ validate: validateServerName }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_server')}
|
||||
|
||||
<FaqTooltip
|
||||
text={
|
||||
<>
|
||||
<div className={s.tooltipText}>
|
||||
{intl.getMessage('encryption_server_tooltip_1')}
|
||||
</div>
|
||||
<div className={s.tooltipText}>
|
||||
{intl.getMessage('encryption_server_tooltip_2')}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={t('encryption_server_enter')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
<Controller
|
||||
name="port_https"
|
||||
control={control}
|
||||
rules={{ validate: { validatePort, validateIsSafePort } }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_https')}
|
||||
|
||||
<FaqTooltip
|
||||
text={intl.getMessage('encryption_https_tooltip')}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={t('encryption_https')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
<Controller
|
||||
name="port_dns_over_tls"
|
||||
control={control}
|
||||
rules={{ validate: validatePortTLS }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_dot')}
|
||||
|
||||
<FaqTooltip text={intl.getMessage('encryption_dot_tooltip')} menuSize="large" />
|
||||
</>
|
||||
}
|
||||
placeholder={t('encryption_dot')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
<Controller
|
||||
name="port_dns_over_quic"
|
||||
control={control}
|
||||
rules={{ validate: validatePortQuic }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_doq')}
|
||||
|
||||
<FaqTooltip text={intl.getMessage('encryption_doq_tooltip')} menuSize="large" />
|
||||
</>
|
||||
}
|
||||
placeholder={t('encryption_doq')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="force_https"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SwitchGroup
|
||||
id="force_https"
|
||||
title={intl.getMessage('encryption_force_redirect')}
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('encryption_certificates')}
|
||||
</h2>
|
||||
|
||||
<p className={cn(s.description, theme.text.t2)}>
|
||||
{intl.getMessage('encryption_certificates_desc', {
|
||||
a: (text: string) => (
|
||||
<a href="https://letsencrypt.org/" target="_blank" rel="noreferrer" className={theme.link.link}>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div className={theme.form.group}>
|
||||
<Controller
|
||||
name="certificate_source"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio
|
||||
value={field.value}
|
||||
handleChange={field.onChange}
|
||||
name={field.name}
|
||||
options={certificateSourceOptions}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<Controller
|
||||
name="certificate_chain"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t('encryption_certificates_input')}
|
||||
disabled={!isEnabled}
|
||||
errorMessage={fieldState.error?.message}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="certificate_path"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_certificate_path')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={handleBlur}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(certificateChain || certificatePath) && (
|
||||
<CertificateStatus
|
||||
validChain={valid_chain}
|
||||
validCert={valid_cert}
|
||||
subject={subject}
|
||||
issuer={issuer}
|
||||
notAfter={not_after}
|
||||
dnsNames={dns_names}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('encryption_key')}
|
||||
</h2>
|
||||
|
||||
<div className={theme.form.group}>
|
||||
<Controller
|
||||
name="key_source"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio
|
||||
value={field.value}
|
||||
handleChange={field.onChange}
|
||||
name={field.name}
|
||||
options={keySourceOptions}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="private_key_saved"
|
||||
control={control}
|
||||
render={({ field: { value, onChange, name } }) => (
|
||||
<Checkbox
|
||||
name={name}
|
||||
disabled={!isEnabled || privateKeySource !== ENCRYPTION_SOURCE.CONTENT}
|
||||
checked={value}
|
||||
onChange={({ target: { checked } }) => {
|
||||
if (checked) {
|
||||
setValue('private_key', '');
|
||||
}
|
||||
onChange(checked);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
className={s.useSavedKey}>
|
||||
{t('use_saved_key')}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
{privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<Controller
|
||||
name="private_key"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t('encryption_key_input')}
|
||||
disabled={!isEnabled || privateKeySaved}
|
||||
errorMessage={fieldState.error?.message}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="private_key_path"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_private_key_path')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={handleBlur}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(privateKey || privateKeyPath) && <KeyStatus validKey={valid_key} />}
|
||||
</div>
|
||||
|
||||
<div className={theme.form.buttonGroup}>
|
||||
{warning_validation && (
|
||||
<ValidationStatus type={isWarning ? 'warning' : 'error'} message={warning_validation} />
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={isDisabled}
|
||||
className={theme.form.button}>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={isSubmitting || processingConfig}
|
||||
onClick={handleResetConfirmOpen}
|
||||
className={theme.form.button}>
|
||||
<Trans>reset_settings</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{openConfirmDialog && (
|
||||
<ConfirmDialog
|
||||
onClose={handleResetConfirmClose}
|
||||
onConfirm={handleReset}
|
||||
buttonText={intl.getMessage('confirm')}
|
||||
cancelText={intl.getMessage('cancel')}
|
||||
title={intl.getMessage('encryption_confirm_clear')}
|
||||
text={intl.getMessage('encryption_confirm_clear_desc')}
|
||||
buttonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
import { EMPTY_DATE } from 'panel/helpers/constants';
|
||||
import intl from 'panel/common/intl';
|
||||
import { StatusBlock } from './StatusBlock';
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
type Props = {
|
||||
validChain: boolean;
|
||||
validCert: boolean;
|
||||
subject?: string;
|
||||
issuer?: string;
|
||||
notAfter?: string;
|
||||
dnsNames?: string[];
|
||||
};
|
||||
|
||||
export const CertificateStatus = ({ validChain, validCert, subject, issuer, notAfter, dnsNames }: Props) => (
|
||||
<StatusBlock
|
||||
variant={validChain ? 'success' : 'error'}
|
||||
title={validChain ? intl.getMessage('encryption_chain_valid') : intl.getMessage('encryption_chain_invalid')}
|
||||
>
|
||||
{validCert && (subject || issuer || notAfter || dnsNames) && (
|
||||
<ul className={s.statusList}>
|
||||
{subject && <li>{intl.getMessage('encryption_subject', { value: subject })}</li>}
|
||||
{issuer && <li>{intl.getMessage('encryption_issuer', { value: issuer })}</li>}
|
||||
{notAfter && notAfter !== EMPTY_DATE && (
|
||||
<li>{intl.getMessage('encryption_expire', { value: format(notAfter, 'YYYY-MM-DD HH:mm:ss') })}</li>
|
||||
)}
|
||||
{dnsNames && <li>{intl.getMessage('encryption_hostnames', { value: dnsNames.join(', ') })}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</StatusBlock>
|
||||
);
|
||||
14
client_v2/src/components/Encryption/Status/KeyStatus.tsx
Normal file
14
client_v2/src/components/Encryption/Status/KeyStatus.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import intl from 'panel/common/intl';
|
||||
import { StatusBlock } from './StatusBlock';
|
||||
|
||||
type Props = {
|
||||
validKey: boolean;
|
||||
};
|
||||
|
||||
export const KeyStatus = ({ validKey }: Props) => (
|
||||
<StatusBlock
|
||||
variant={validKey ? 'success' : 'error'}
|
||||
title={validKey ? intl.getMessage('encryption_key_valid') : intl.getMessage('encryption_key_invalid')}
|
||||
/>
|
||||
);
|
||||
28
client_v2/src/components/Encryption/Status/StatusBlock.tsx
Normal file
28
client_v2/src/components/Encryption/Status/StatusBlock.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import cn from 'clsx';
|
||||
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
export type StatusVariant = 'success' | 'error' | 'warning';
|
||||
|
||||
type Props = {
|
||||
variant: StatusVariant;
|
||||
title: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const StatusBlock = ({ variant, title, children }: Props) => (
|
||||
<div className={cn(s.status, theme.text.t3)}>
|
||||
<div
|
||||
className={cn(s.statusTitle, {
|
||||
[s.statusTitle_success]: variant === 'success',
|
||||
[s.statusTitle_error]: variant === 'error',
|
||||
[s.statusTitle_warning]: variant === 'warning',
|
||||
})}>
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { StatusBlock } from './StatusBlock';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
type Props = {
|
||||
type: 'warning' | 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const ValidationStatus = ({ type, message }: Props) => (
|
||||
<StatusBlock variant={type} title={intl.getMessage('encryption_certificate_has_issues')}>
|
||||
<div className={s.statusText}>{message}</div>
|
||||
</StatusBlock>
|
||||
);
|
||||
3
client_v2/src/components/Encryption/Status/index.ts
Normal file
3
client_v2/src/components/Encryption/Status/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { CertificateStatus } from './CertificateStatus';
|
||||
export { KeyStatus } from './KeyStatus';
|
||||
export { ValidationStatus } from './ValidationStatus';
|
||||
@ -0,0 +1,47 @@
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 16px;
|
||||
color: var(--default-main-text);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--main-box-shadow);
|
||||
background-color: var(--fills-backgrounds-page-background-additional);
|
||||
cursor: default;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: calc(100% + 56px);
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
.statusTitle {
|
||||
padding: 8px 0;
|
||||
font-weight: var(--weight-semi-bold);
|
||||
|
||||
&_success {
|
||||
color: var(--default-link);
|
||||
}
|
||||
|
||||
&_warning {
|
||||
color: var(--default-attention-link);
|
||||
}
|
||||
|
||||
&_error {
|
||||
color: var(--default-error-link);
|
||||
}
|
||||
}
|
||||
|
||||
.statusList {
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.statusText {
|
||||
padding: 8px 0;
|
||||
}
|
||||
115
client_v2/src/components/Encryption/index.tsx
Normal file
115
client_v2/src/components/Encryption/index.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from 'panel/helpers/constants';
|
||||
import { RootState } from 'panel/initialState';
|
||||
import { PageLoader } from 'panel/common/ui/Loader';
|
||||
import { setTlsConfig, validateTlsConfig } from 'panel/actions/encryption';
|
||||
import theme from 'panel/lib/theme';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
import { EncryptionFormValues, Form } from './Form';
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
export const Encryption = () => {
|
||||
const dispatch = useDispatch();
|
||||
const encryption = useSelector((state: RootState) => state.encryption);
|
||||
|
||||
const initialValues = useMemo((): EncryptionFormValues => {
|
||||
const {
|
||||
enabled,
|
||||
serve_plain_dns,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
port_dns_over_quic,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
certificate_path,
|
||||
private_key_path,
|
||||
private_key_saved,
|
||||
} = encryption;
|
||||
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
||||
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
||||
|
||||
return {
|
||||
enabled,
|
||||
serve_plain_dns,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
port_dns_over_quic,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
certificate_path,
|
||||
private_key_path,
|
||||
private_key_saved,
|
||||
certificate_source,
|
||||
key_source,
|
||||
};
|
||||
}, [encryption]);
|
||||
|
||||
const getSubmitValues = useCallback((values: any) => {
|
||||
const { certificate_source, key_source, private_key_saved, ...config } = values;
|
||||
|
||||
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
|
||||
config.certificate_chain = '';
|
||||
} else {
|
||||
config.certificate_path = '';
|
||||
}
|
||||
|
||||
if (key_source === ENCRYPTION_SOURCE.PATH) {
|
||||
config.private_key = '';
|
||||
} else {
|
||||
config.private_key_path = '';
|
||||
|
||||
if (private_key_saved) {
|
||||
config.private_key = '';
|
||||
config.private_key_saved = private_key_saved;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(values: any) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
dispatch(setTlsConfig(submitValues));
|
||||
},
|
||||
[getSubmitValues, setTlsConfig],
|
||||
);
|
||||
|
||||
const validateConfig = useCallback((values) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
|
||||
if (submitValues.enabled) {
|
||||
dispatch(validateTlsConfig(submitValues));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debouncedConfigValidation = useMemo(() => debounce(validateConfig, DEBOUNCE_TIMEOUT), [validateConfig]);
|
||||
|
||||
return (
|
||||
<div className={theme.layout.container}>
|
||||
<h1 className={cn(theme.layout.title, theme.title.h4, theme.title.h3_tablet, s.title)}>
|
||||
{intl.getMessage('encryption_title')}
|
||||
</h1>
|
||||
|
||||
{encryption.processing ? (
|
||||
<PageLoader />
|
||||
) : (
|
||||
<Form
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
debouncedConfigValidation={debouncedConfigValidation}
|
||||
encryption={encryption}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
16
client_v2/src/components/Encryption/styles.module.pcss
Normal file
16
client_v2/src/components/Encryption/styles.module.pcss
Normal file
@ -0,0 +1,16 @@
|
||||
.title {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.useSavedKey {
|
||||
padding: 8px 0 8px 32px;
|
||||
}
|
||||
|
||||
.tooltipText:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -1,14 +1,13 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { RoutePath } from 'panel/components/Routes/Paths';
|
||||
import { Link } from 'panel/common/ui/Link';
|
||||
|
||||
import { setFiltersConfig } from 'panel/actions/filtering';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { SwitchGroup } from './SettingsGroup';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
|
||||
@ -5,12 +5,11 @@ import cn from 'clsx';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import intl from 'panel/common/intl';
|
||||
import { trimLinesAndRemoveEmpty } from 'panel/helpers/helpers';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
|
||||
import s from './styles.module.pcss';
|
||||
import { SwitchGroup } from '../SettingsGroup';
|
||||
|
||||
type Props = {
|
||||
control: Control<any>;
|
||||
@ -38,30 +37,25 @@ export const IgnoredDomains = ({ control, processing, ignoreEnabled, setValue, s
|
||||
{...field}
|
||||
id={textareaId}
|
||||
label={
|
||||
<div className={s.label}>
|
||||
<>
|
||||
{intl.getMessage('settings_domain_names')}
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
menu={
|
||||
<div className={cn(theme.dropdown.menu, s.dropdownMenu)}>
|
||||
|
||||
<FaqTooltip
|
||||
text={
|
||||
<>
|
||||
<div className={s.dropdownTitle}>
|
||||
{intl.getMessage('settings_tooltip_domain_names')}
|
||||
</div>
|
||||
<div className={s.dropdownText}>
|
||||
<div>
|
||||
<strong>{intl.getMessage('settings_tooltip_examples')}</strong>
|
||||
<div>example.com</div>
|
||||
<div>*.example.com</div>
|
||||
<div>||example.com^</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
className={s.dropdown}
|
||||
position="bottomLeft"
|
||||
noIcon>
|
||||
<div className={s.dropdownTrigger}>
|
||||
<Icon icon="faq" className={s.icon} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
/>
|
||||
|
||||
<a
|
||||
href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax&from=ui&app=home"
|
||||
target="_blank"
|
||||
@ -69,7 +63,7 @@ export const IgnoredDomains = ({ control, processing, ignoreEnabled, setValue, s
|
||||
className={cn(s.link, theme.link.link, theme.link.noDecoration)}>
|
||||
{intl.getMessage('settings_rule_syntax')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
placeholder={`example.com\n*.example.com\n||example.com^`}
|
||||
size="large"
|
||||
|
||||
@ -9,20 +9,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
cursor: pointer;
|
||||
color: var(--default-gray-icons);
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
padding: 16px;
|
||||
max-width: 240px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownTitle {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@ -6,8 +6,7 @@ import { Button } from 'panel/common/ui/Button';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { QUERY_LOG_INTERVALS_DAYS, RETENTION_CUSTOM } from 'panel/helpers/constants';
|
||||
|
||||
import { RadioGroup } from '../SettingsGroup/RadioGroup';
|
||||
import { SwitchGroup } from '../SettingsGroup';
|
||||
import { RadioGroup, SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
import { IgnoredDomains } from '../IgnoredDomains';
|
||||
import { getIntervalTitle, getDefaultInterval } from '../helpers';
|
||||
import { RetentionCustomInput } from '../RetentionCustomInput';
|
||||
|
||||
@ -11,12 +11,12 @@ import { initSettings, toggleSetting } from 'panel/actions';
|
||||
import { getStatsConfig } from 'panel/actions/stats';
|
||||
import { getLogsConfig } from 'panel/actions/queryLogs';
|
||||
import { getFilteringStatus } from 'panel/actions/filtering';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
import { StatsConfig } from './StatsConfig/StatsConfig';
|
||||
import { LogsConfig } from './LogsConfig';
|
||||
import { FiltersConfig } from './FiltersConfig';
|
||||
import { getSafeSearchProviderTitle } from './helpers';
|
||||
import { SwitchGroup } from './SettingsGroup';
|
||||
|
||||
const SETTINGS = {
|
||||
safebrowsing: {
|
||||
@ -48,9 +48,8 @@ export const Settings = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSettingToggle =
|
||||
(key: keyof typeof SETTINGS) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(toggleSetting(key, !e.target.checked));
|
||||
const handleSettingToggle = (key: keyof typeof SETTINGS) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(toggleSetting(key, !e.target.checked));
|
||||
|
||||
const renderSettings = (settingsList?: SettingsData['settingsList']) =>
|
||||
settingsList
|
||||
@ -82,20 +81,18 @@ export const Settings = () => {
|
||||
|
||||
type SafeSearchConfigShape = Record<string, boolean> & { enabled: boolean };
|
||||
|
||||
const onSafeSearchEnabledChange =
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = { ...safesearch, enabled: e.target.checked } as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
const onSafeSearchEnabledChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = { ...safesearch, enabled: e.target.checked } as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
|
||||
const onProviderChange =
|
||||
(searchKey: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = {
|
||||
...safesearch,
|
||||
[searchKey]: e.target.checked,
|
||||
} as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
const onProviderChange = (searchKey: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = {
|
||||
...safesearch,
|
||||
[searchKey]: e.target.checked,
|
||||
} as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
|
||||
return (
|
||||
<SwitchGroup
|
||||
|
||||
@ -4,10 +4,10 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import intl from 'panel/common/intl';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { getIntervalTitle, getDefaultInterval } from '../helpers';
|
||||
import { RadioGroup, SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
import { getIntervalTitle, getDefaultInterval } from '../helpers';
|
||||
import { STATS_INTERVALS_DAYS, RETENTION_CUSTOM } from '../../../helpers/constants';
|
||||
import { RadioGroup, SwitchGroup } from '../SettingsGroup';
|
||||
import { IgnoredDomains } from '../IgnoredDomains';
|
||||
import { RetentionCustomInput } from '../RetentionCustomInput';
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
.buttonGroup {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@ -28,3 +29,8 @@
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user