diff --git a/client_v2/src/__locales/en.json b/client_v2/src/__locales/en.json index b6c92604..b5d11521 100644 --- a/client_v2/src/__locales/en.json +++ b/client_v2/src/__locales/en.json @@ -97,7 +97,6 @@ "settings": "Settings", "filters": "Filters", "filter": "Filter", - "query_log": "Query Log", "compact": "Compact", "nothing_found": "Nothing found", "faq": "FAQ", @@ -195,7 +194,6 @@ "no_whitelist_added": "No allowlists added", "add_blocklist": "Add blocklist", "add_allowlist": "Add allowlist", - "cancel_btn": "Cancel", "enter_name_hint": "Enter name", "enter_url_or_path_hint": "Enter a URL or an absolute path of the list", "check_updates_btn": "Check for updates", @@ -452,10 +450,8 @@ "clients_title": "Persistent clients", "clients_desc": "Configure persistent client records for devices connected to AdGuard Home", "settings_global": "Global", - "settings_custom": "Custom", "table_client": "Client", "table_name": "Name", - "save_btn": "Save", "client_add": "Add Client", "client_new": "New Client", "client_edit": "Edit Client", @@ -762,5 +758,52 @@ "release_notes": "Release notes", "privacy_policy": "Privacy policy", "selected": "%value% selected", - "general_settings_short": "General" + "settings_general_short": "General", + "settings_filtering_and_security": "Filtering and security", + "settings_filter_requests": "Filter requests", + "settings_filter_requests_desc": "You can use Blocklists, Allowlists, and User rules to set up filtering rules", + "settings_browsing_security": "Browsing security", + "settings_browsing_security_desc": "Block access to domains found in the database of malicious and phishing domains", + "settings_parental_control": "Parental control", + "settings_parental_control_desc": "Block websites with content inappropriate for children", + "settings_safe_search": "Safe search", + "settings_safe_search_desc": "Block inappropriate content in search results", + "settings_log_dns_requests": "Log DNS requests", + "settings_anonymize_client_ip": "Anonymize client IP addresses", + "settings_anonymize_client_ip_desc": "Replace part of the IP address with zeros", + "settings_log_rotation": "Log rotation", + "settings_hours": "| %count% hour | %count% hours", + "settings_days": "| %count% day | %count% days", + "settings_custom": "Custom", + "settings_log_rotation_hours": "Log rotation, in hours", + "settings_rotation_placeholder": "Use numbers", + "settings_log_ignored_domains": "Ignored domains", + "settings_log_ignored_domains_desc": "Requests that match these rules are not displayed in Query log", + "settings_domain_names": "Domain names, one per line", + "settings_rule_syntax": "Rule syntax", + "save": "Save", + "cancel": "Cancel", + "query_log": "Query Log", + "clear_query_log": "Clear Query log", + "settings_statistics": "Statistics", + "settings_statistics_desc": "Display request and client statistics on the dashboard", + "settings_statistics_retention": "Statistics retention", + "settings_statistics_retention_hours": "Statistics retention, in hours", + "settings_statistics_ignored_domains": "Requests that match these rules are not included in the statistics", + "settings_statistics_clear": "Clear statistics", + "settings_confirm_decrease_log_rotation_interval": "Decrease log rotation interval?", + "settings_confirm_decrease_log_rotation_interval_desc": "This will delete all logs older than %value%", + "settings_yes_decrease": "Yes, decrease", + "settings_confirm_decrease_stats_rotation_interval": "Decrease stats retention interval?", + "settings_confirm_decrease_stats_rotation_interval_desc": "This will delete all stats older than %value%", + "settings_confirm_clear_statistics": "Clear statistics?", + "settings_confirm_clear_statistics_desc": "This will clear all statistics. Query log will still be available", + "settings_yes_clear": "Yes, clear", + "settings_confirm_clear_query_log": "Clear Query log?", + "settings_confirm_clear_query_log_desc": "This will delete all requests from Query log. Statistics will still be available", + "settings_tooltip_domain_names": "Domain names, wildcards, or filtering rules", + "settings_tooltip_examples": "Examples:", + "settings_notify_changes_saved": "Changes saved", + "settings_notify_query_log_cleared": "Query log cleared", + "settings_notify_statistics_cleared": "Statistics cleared" } diff --git a/client_v2/src/actions/dnsConfig.ts b/client_v2/src/actions/dnsConfig.ts index 09eeb533..9b9b996c 100644 --- a/client_v2/src/actions/dnsConfig.ts +++ b/client_v2/src/actions/dnsConfig.ts @@ -5,6 +5,7 @@ import { apiClient } from '../api/Api'; import { splitByNewLine } from '../helpers/helpers'; import { addErrorToast, addSuccessToast } from './toasts'; +import intl from 'panel/common/intl'; export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST'); export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE'); @@ -30,7 +31,7 @@ export const clearDnsCache = () => async (dispatch: any) => { try { const data = await apiClient.clearCache(); dispatch(clearDnsCacheSuccess(data)); - dispatch(addSuccessToast(i18next.t('cache_cleared'))); + dispatch(addSuccessToast(intl.getMessage('cache_cleared'))); } catch (error) { dispatch(addErrorToast({ error })); dispatch(clearDnsCacheFailure()); @@ -71,9 +72,9 @@ export const setDnsConfig = (config: any) => async (dispatch: any) => { await apiClient.setDnsConfig(data); if (hasDnsSettings) { - dispatch(addSuccessToast('updated_upstream_dns_toast')); + dispatch(addSuccessToast(intl.getMessage('updated_upstream_dns_toast'))); } else { - dispatch(addSuccessToast('config_successfully_saved')); + dispatch(addSuccessToast(intl.getMessage('settings_notify_changes_saved'))); } dispatch(setDnsConfigSuccess(config)); diff --git a/client_v2/src/actions/encryption.ts b/client_v2/src/actions/encryption.ts index 003aec22..d3bbd90c 100644 --- a/client_v2/src/actions/encryption.ts +++ b/client_v2/src/actions/encryption.ts @@ -3,6 +3,7 @@ import { apiClient } from '../api/Api'; import { redirectToCurrentProtocol } from '../helpers/helpers'; import { addErrorToast, addSuccessToast } from './toasts'; +import intl from 'panel/common/intl'; export const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST'); export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE'); @@ -57,7 +58,7 @@ export const setTlsConfig = (config: any) => async (dispatch: any, getState: any } dispatch(setTlsConfigSuccess(response)); - dispatch(addSuccessToast('encryption_config_saved')); + dispatch(addSuccessToast(intl.getMessage('encryption_config_saved'))); } catch (error) { dispatch(addErrorToast({ error })); dispatch(setTlsConfigFailure()); diff --git a/client_v2/src/actions/filtering.ts b/client_v2/src/actions/filtering.ts index 1fcbcb9a..ac777212 100644 --- a/client_v2/src/actions/filtering.ts +++ b/client_v2/src/actions/filtering.ts @@ -154,19 +154,10 @@ export const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST' export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE'); export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS'); -export const setFiltersConfig = (config: any) => async (dispatch: any, getState: any) => { +export const setFiltersConfig = (config: any) => async (dispatch: any) => { dispatch(setFiltersConfigRequest()); try { - const { enabled } = config; - const prevEnabled = getState().filtering.enabled; - let successToastMessage = 'config_successfully_saved'; - - if (prevEnabled !== enabled) { - successToastMessage = enabled ? 'enabled_filtering_toast' : 'disabled_filtering_toast'; - } - await apiClient.setFiltersConfig(config); - dispatch(addSuccessToast(successToastMessage)); dispatch(setFiltersConfigSuccess(config)); } catch (error) { dispatch(addErrorToast({ error })); diff --git a/client_v2/src/actions/index.tsx b/client_v2/src/actions/index.tsx index 4b4e290b..7ca4fd2a 100644 --- a/client_v2/src/actions/index.tsx +++ b/client_v2/src/actions/index.tsx @@ -27,72 +27,83 @@ import { getTlsStatus } from './encryption'; import { apiClient } from '../api/Api'; import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts'; import { getFilteringStatus, setRules } from './filtering'; +import type { Dispatch } from 'redux'; +import type { SettingsData } from '../initialState'; +import intl from 'panel/common/intl'; -export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE'); +export const toggleSettingStatus = createAction<{ + settingKey: keyof typeof SETTINGS_NAMES; + value?: boolean | SafeSearchConfig; +}>('SETTING_STATUS_TOGGLE'); export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW'); +// Types for settings actions +type SafeSearchConfig = Record & { enabled: boolean }; +type ToggleSettingKey = keyof typeof SETTINGS_NAMES; +type SettingsItem = NonNullable['safebrowsing']; +type InitSettingsItemInput = Partial> & + Record; +type InitSettingsArg = { + safebrowsing?: InitSettingsItemInput; + parental?: InitSettingsItemInput; +}; + /** * * @param {*} settingKey = SETTINGS_NAMES * @param {*} status: boolean | SafeSearchConfig * @returns */ -export const toggleSetting = (settingKey: any, status: any) => async (dispatch: any) => { - let successMessage = ''; - try { - switch (settingKey) { - case SETTINGS_NAMES.safebrowsing: - if (status) { - successMessage = 'disabled_safe_browsing_toast'; - await apiClient.disableSafebrowsing(); - } else { - successMessage = 'enabled_safe_browsing_toast'; - await apiClient.enableSafebrowsing(); - } - dispatch(toggleSettingStatus({ settingKey })); - break; - case SETTINGS_NAMES.parental: - if (status) { - successMessage = 'disabled_parental_toast'; - await apiClient.disableParentalControl(); - } else { - successMessage = 'enabled_parental_toast'; - await apiClient.enableParentalControl(); - } - dispatch(toggleSettingStatus({ settingKey })); - break; - case SETTINGS_NAMES.safesearch: - successMessage = 'updated_save_search_toast'; - await apiClient.updateSafesearch(status); - dispatch(toggleSettingStatus({ settingKey, value: status })); - break; - default: - break; +export const toggleSetting = + (settingKey: ToggleSettingKey, status: boolean | SafeSearchConfig) => async (dispatch: Dispatch) => { + try { + switch (settingKey) { + case SETTINGS_NAMES.safebrowsing: + if (status) { + await apiClient.disableSafebrowsing(); + } else { + await apiClient.enableSafebrowsing(); + } + dispatch(toggleSettingStatus({ settingKey })); + break; + case SETTINGS_NAMES.parental: + if (status) { + await apiClient.disableParentalControl(); + } else { + await apiClient.enableParentalControl(); + } + dispatch(toggleSettingStatus({ settingKey })); + break; + case SETTINGS_NAMES.safesearch: + await apiClient.updateSafesearch(status as SafeSearchConfig); + dispatch(toggleSettingStatus({ settingKey, value: status })); + break; + default: + break; + } + } catch (error) { + dispatch(addErrorToast({ error })); } - dispatch(addSuccessToast(successMessage)); - } catch (error) { - dispatch(addErrorToast({ error })); - } -}; + }; export const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST'); export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE'); -export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS'); +type SettingsSuccessList = { + safebrowsing: Record & { enabled: boolean }; + parental: Record & { enabled: boolean }; + safesearch: SafeSearchConfig; +}; +export const initSettingsSuccess = createAction<{ settingsList: SettingsSuccessList }>('SETTINGS_INIT_SUCCESS'); export const initSettings = - ( - settingsList = { - safebrowsing: {}, - parental: {}, - }, - ) => - async (dispatch: any) => { + (settingsList: InitSettingsArg = { safebrowsing: {}, parental: {} }) => + async (dispatch: Dispatch) => { dispatch(initSettingsRequest()); try { const safebrowsingStatus = await apiClient.getSafebrowsingStatus(); const parentalStatus = await apiClient.getParentalStatus(); - const safesearchStatus = await apiClient.getSafesearchStatus(); - const { safebrowsing, parental } = settingsList; + const safesearchStatus = (await apiClient.getSafesearchStatus()) as SafeSearchConfig; + const { safebrowsing = {}, parental = {} } = settingsList; const newSettingsList = { safebrowsing: { ...safebrowsing, @@ -120,19 +131,21 @@ export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS') const getDisabledMessage = (time: any) => { switch (time) { case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE: - return i18next.t('disable_notify_for_seconds', { + return intl.getMessage('disable_notify_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE), }); case DISABLE_PROTECTION_TIMINGS.MINUTE: - return i18next.t('disable_notify_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }); + return intl.getMessage('disable_notify_for_minutes', { + count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE), + }); case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES: - return i18next.t('disable_notify_for_minutes', { + return intl.getMessage('disable_notify_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES), }); case DISABLE_PROTECTION_TIMINGS.HOUR: - return i18next.t('disable_notify_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }); + return intl.getMessage('disable_notify_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }); case DISABLE_PROTECTION_TIMINGS.TOMORROW: - return i18next.t('disable_notify_until_tomorrow'); + return intl.getMessage('disable_notify_until_tomorrow'); default: return 'disabled_protection'; } @@ -143,7 +156,7 @@ export const toggleProtection = async (dispatch: any) => { dispatch(toggleProtectionRequest()); try { - const successMessage = status ? getDisabledMessage(time) : 'enabled_protection'; + const successMessage = status ? getDisabledMessage(time) : intl.getMessage('enabled_protection'); await apiClient.setProtection({ enabled: !status, duration: time }); dispatch(addSuccessToast(successMessage)); dispatch(toggleProtectionSuccess({ disabledDuration: time })); @@ -176,9 +189,9 @@ export const getVersion = const currentVersion = dnsVersion === 'undefined' ? 0 : dnsVersion; if (data && !areEqualVersions(currentVersion, data.new_version)) { - dispatch(addSuccessToast('updates_checked')); + dispatch(addSuccessToast(intl.getMessage('updates_checked'))); } else { - dispatch(addSuccessToast('updates_version_equal')); + dispatch(addSuccessToast(intl.getMessage('updates_version_equal'))); } } } catch (error) { @@ -413,7 +426,7 @@ export const testUpstream = }); if (testMessages.every((message) => message === 'OK' || message.startsWith('WARNING:'))) { - dispatch(addSuccessToast('dns_test_ok_toast')); + dispatch(addSuccessToast(intl.getMessage('dns_test_ok_toast'))); } dispatch(testUpstreamSuccess()); @@ -572,7 +585,7 @@ export const findActiveDhcp = (selectedInterface: any) => async (dispatch: any, }); dispatch(addErrorToast({ error: warning })); } else { - dispatch(addSuccessToast('dhcp_not_found')); + dispatch(addSuccessToast(intl.getMessage('dhcp_not_found'))); } } catch (error) { dispatch(addErrorToast({ error })); @@ -589,7 +602,7 @@ export const setDhcpConfig = (values: any) => async (dispatch: any) => { try { await apiClient.setDhcpConfig(values); dispatch(setDhcpConfigSuccess(values)); - dispatch(addSuccessToast('dhcp_config_saved')); + dispatch(addSuccessToast(intl.getMessage('dhcp_config_saved'))); } catch (error) { dispatch(addErrorToast({ error })); dispatch(setDhcpConfigFailure()); @@ -606,14 +619,14 @@ export const toggleDhcp = (values: any) => async (dispatch: any) => { ...values, enabled: false, }; - let successMessage = 'disabled_dhcp'; + let successMessage = intl.getMessage('disabled_dhcp'); if (!values.enabled) { config = { ...values, enabled: true, }; - successMessage = 'enabled_dhcp'; + successMessage = intl.getMessage('enabled_dhcp'); } try { @@ -635,7 +648,7 @@ export const resetDhcp = () => async (dispatch: any) => { try { const status = await apiClient.resetDhcp(); dispatch(resetDhcpSuccess(status)); - dispatch(addSuccessToast('dhcp_config_saved')); + dispatch(addSuccessToast(intl.getMessage('dhcp_config_saved'))); } catch (error) { dispatch(addErrorToast({ error })); dispatch(resetDhcpFailure()); @@ -651,7 +664,7 @@ export const resetDhcpLeases = () => async (dispatch: any) => { try { const status = await apiClient.resetDhcpLeases(); dispatch(resetDhcpLeasesSuccess(status)); - dispatch(addSuccessToast('dhcp_reset_leases_success')); + dispatch(addSuccessToast(intl.getMessage('dhcp_reset_leases_success'))); } catch (error) { dispatch(addErrorToast({ error })); dispatch(resetDhcpLeasesFailure()); @@ -670,7 +683,7 @@ export const addStaticLease = (config: any) => async (dispatch: any) => { const name = config.hostname || config.ip; await apiClient.addStaticLease(config); dispatch(addStaticLeaseSuccess(config)); - dispatch(addSuccessToast(i18next.t('dhcp_lease_added', { key: name }))); + dispatch(addSuccessToast(intl.getMessage('dhcp_lease_added', { key: name }))); dispatch(toggleLeaseModal()); dispatch(getDhcpStatus()); } catch (error) { @@ -689,7 +702,7 @@ export const removeStaticLease = (config: any) => async (dispatch: any) => { const name = config.hostname || config.ip; await apiClient.removeStaticLease(config); dispatch(removeStaticLeaseSuccess(config)); - dispatch(addSuccessToast(i18next.t('dhcp_lease_deleted', { key: name }))); + dispatch(addSuccessToast(intl.getMessage('dhcp_lease_deleted', { key: name }))); } catch (error) { dispatch(addErrorToast({ error })); dispatch(removeStaticLeaseFailure()); @@ -705,7 +718,7 @@ export const updateStaticLease = (config: any) => async (dispatch: any) => { try { await apiClient.updateStaticLease(config); dispatch(updateStaticLeaseSuccess(config)); - dispatch(addSuccessToast(i18next.t('dhcp_lease_updated', { key: config.hostname || config.ip }))); + dispatch(addSuccessToast(intl.getMessage('dhcp_lease_updated', { key: config.hostname || config.ip }))); dispatch(toggleLeaseModal()); dispatch(getDhcpStatus()); } catch (error) { @@ -734,15 +747,23 @@ export const toggleBlocking = if (matchPreparedBlockingRule) { await dispatch(setRules(userRules.replace(`${blockingRule}`, ''))); - dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); + dispatch( + addSuccessToast(intl.getMessage('rule_removed_from_custom_filtering_toast', { rule: blockingRule })), + ); } else if (!matchPreparedUnblockingRule) { await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`)); - dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); + dispatch( + addSuccessToast(intl.getMessage('rule_added_to_custom_filtering_toast', { rule: unblockingRule })), + ); } else if (matchPreparedUnblockingRule) { - dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); + dispatch( + addSuccessToast(intl.getMessage('rule_added_to_custom_filtering_toast', { rule: unblockingRule })), + ); return; } else if (!matchPreparedBlockingRule) { - dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); + dispatch( + addSuccessToast(intl.getMessage('rule_removed_from_custom_filtering_toast', { rule: blockingRule })), + ); return; } diff --git a/client_v2/src/actions/stats.ts b/client_v2/src/actions/stats.ts index 3b0965bc..7987360a 100644 --- a/client_v2/src/actions/stats.ts +++ b/client_v2/src/actions/stats.ts @@ -27,7 +27,7 @@ export const setStatsConfig = (config: any) => async (dispatch: any) => { dispatch(setStatsConfigRequest()); try { await apiClient.setStatsConfig(config); - dispatch(addSuccessToast('config_successfully_saved')); + dispatch(addSuccessToast('settings_notify_changes_saved')); dispatch(setStatsConfigSuccess(config)); } catch (error) { dispatch(addErrorToast({ error })); @@ -74,7 +74,7 @@ export const resetStats = () => async (dispatch: any) => { dispatch(getStatsRequest()); try { await apiClient.resetStats(); - dispatch(addSuccessToast('statistics_cleared')); + dispatch(addSuccessToast('settings_notify_statistics_cleared')); dispatch(resetStatsSuccess()); } catch (error) { dispatch(addErrorToast({ error })); diff --git a/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss b/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss index aaf9dc3d..0561a769 100644 --- a/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss +++ b/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss @@ -3,6 +3,10 @@ align-items: center; cursor: pointer; gap: 8px; + + &.disabled { + cursor: default; + } } .input { @@ -45,11 +49,9 @@ position: absolute; width: 24px; height: 24px; - transition: color var(--t2); color: var(--default-gray-icons); } .active { - transition: none; color: var(--default-product-icon); } diff --git a/client_v2/src/common/controls/Checkbox/Checkbox.tsx b/client_v2/src/common/controls/Checkbox/Checkbox.tsx index bb42163a..6dc90b8d 100644 --- a/client_v2/src/common/controls/Checkbox/Checkbox.tsx +++ b/client_v2/src/common/controls/Checkbox/Checkbox.tsx @@ -27,7 +27,7 @@ export const Checkbox = ({ onClick, plusStyle, }: Props) => ( -