mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-10-26 11:27:18 +00:00
ADG-10293
This commit is contained in:
parent
b3ca6c9934
commit
f7357be494
@ -28,7 +28,6 @@ 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<{
|
||||
@ -37,16 +36,8 @@ export const toggleSettingStatus = createAction<{
|
||||
}>('SETTING_STATUS_TOGGLE');
|
||||
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||
|
||||
// Types for settings actions
|
||||
type SafeSearchConfig = Record<string, boolean> & { enabled: boolean };
|
||||
type ToggleSettingKey = keyof typeof SETTINGS_NAMES;
|
||||
type SettingsItem = NonNullable<SettingsData['settingsList']>['safebrowsing'];
|
||||
type InitSettingsItemInput = Partial<Pick<SettingsItem, 'enabled' | 'order' | 'subtitle' | 'title'>> &
|
||||
Record<string, unknown>;
|
||||
type InitSettingsArg = {
|
||||
safebrowsing?: InitSettingsItemInput;
|
||||
parental?: InitSettingsItemInput;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@ -89,40 +80,35 @@ export const toggleSetting =
|
||||
export const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST');
|
||||
export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');
|
||||
type SettingsSuccessList = {
|
||||
safebrowsing: Record<string, unknown> & { enabled: boolean };
|
||||
parental: Record<string, unknown> & { enabled: boolean };
|
||||
safebrowsing: { enabled: boolean };
|
||||
parental: { enabled: boolean };
|
||||
safesearch: SafeSearchConfig;
|
||||
};
|
||||
export const initSettingsSuccess = createAction<{ settingsList: SettingsSuccessList }>('SETTINGS_INIT_SUCCESS');
|
||||
|
||||
export const initSettings =
|
||||
(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()) as SafeSearchConfig;
|
||||
const { safebrowsing = {}, parental = {} } = settingsList;
|
||||
const newSettingsList = {
|
||||
safebrowsing: {
|
||||
...safebrowsing,
|
||||
enabled: safebrowsingStatus.enabled,
|
||||
},
|
||||
parental: {
|
||||
...parental,
|
||||
enabled: parentalStatus.enabled,
|
||||
},
|
||||
safesearch: {
|
||||
...safesearchStatus,
|
||||
},
|
||||
};
|
||||
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
}
|
||||
};
|
||||
export const initSettings = () => async (dispatch: Dispatch) => {
|
||||
dispatch(initSettingsRequest());
|
||||
try {
|
||||
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
|
||||
const parentalStatus = await apiClient.getParentalStatus();
|
||||
const safesearchStatus = (await apiClient.getSafesearchStatus()) as SafeSearchConfig;
|
||||
const newSettingsList: SettingsSuccessList = {
|
||||
safebrowsing: {
|
||||
enabled: safebrowsingStatus.enabled,
|
||||
},
|
||||
parental: {
|
||||
enabled: parentalStatus.enabled,
|
||||
},
|
||||
safesearch: {
|
||||
...safesearchStatus,
|
||||
},
|
||||
};
|
||||
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
|
||||
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
|
||||
|
||||
@ -5,6 +5,7 @@ import { apiClient } from '../api/Api';
|
||||
import { normalizeLogs } from '../helpers/helpers';
|
||||
import { DEFAULT_LOGS_FILTER, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
type SearchFormValues = {
|
||||
search: string;
|
||||
@ -173,7 +174,7 @@ export const clearLogs = () => async (dispatch: any) => {
|
||||
try {
|
||||
await apiClient.clearQueryLog();
|
||||
dispatch(clearLogsSuccess());
|
||||
dispatch(addSuccessToast('query_log_cleared'));
|
||||
dispatch(addSuccessToast(intl.getMessage('settings_notify_query_log_cleared')));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(clearLogsFailure(error));
|
||||
@ -203,7 +204,7 @@ export const setLogsConfig = (config: any) => async (dispatch: any) => {
|
||||
dispatch(setLogsConfigRequest());
|
||||
try {
|
||||
await apiClient.setQueryLogConfig(config);
|
||||
dispatch(addSuccessToast('config_successfully_saved'));
|
||||
dispatch(addSuccessToast(intl.getMessage('settings_notify_changes_saved')));
|
||||
dispatch(setLogsConfigSuccess(config));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
|
||||
@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
|
||||
import { apiClient } from '../api/Api';
|
||||
import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
|
||||
export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
|
||||
@ -27,7 +28,7 @@ export const setStatsConfig = (config: any) => async (dispatch: any) => {
|
||||
dispatch(setStatsConfigRequest());
|
||||
try {
|
||||
await apiClient.setStatsConfig(config);
|
||||
dispatch(addSuccessToast('settings_notify_changes_saved'));
|
||||
dispatch(addSuccessToast(intl.getMessage('settings_notify_changes_saved')));
|
||||
dispatch(setStatsConfigSuccess(config));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
@ -74,7 +75,7 @@ export const resetStats = () => async (dispatch: any) => {
|
||||
dispatch(getStatsRequest());
|
||||
try {
|
||||
await apiClient.resetStats();
|
||||
dispatch(addSuccessToast('settings_notify_statistics_cleared'));
|
||||
dispatch(addSuccessToast(intl.getMessage('settings_notify_statistics_cleared')));
|
||||
dispatch(resetStatsSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
|
||||
@ -14,14 +14,14 @@
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
padding: 0 16px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
padding: 0 var(--m-xl);
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ export const ICONS = {
|
||||
loader: 'loader',
|
||||
check: 'check',
|
||||
dot: 'dot',
|
||||
attention: 'attention',
|
||||
} as const;
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
@ -319,6 +320,33 @@ export const Icons = memo(() => (
|
||||
fill="currentColor"
|
||||
/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="attention" viewBox="0 0 24 24" fill="none">
|
||||
<circle
|
||||
cx="9"
|
||||
cy="9"
|
||||
r="9"
|
||||
transform="matrix(1 0 0 -1 3 21)"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 8V14"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.997 16.4045C12.009 16.4025 11.997 16.5955 11.997 16.5955"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
));
|
||||
Icons.displayName = 'Icons';
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.chartOverlay,
|
||||
.modalOverlay {
|
||||
.pageOverlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -39,18 +39,23 @@
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
.pageOverlay {
|
||||
min-height: 440px;
|
||||
}
|
||||
|
||||
.chartLoader,
|
||||
.modalLoader {
|
||||
.chartLoader {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
color: var(--default-link);
|
||||
color: var(--default-product-icon);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
.pageLoader {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
color: var(--default-product-icon);
|
||||
}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Loader } from './Loader';
|
||||
|
||||
import s from './Loader.module.pcss';
|
||||
|
||||
export const ModalLoader = () => <Loader overlayClassName={s.modalOverlay} className={s.modalLoader} icon="loader" />;
|
||||
6
client_v2/src/common/ui/Loader/PageLoader.tsx
Normal file
6
client_v2/src/common/ui/Loader/PageLoader.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Loader } from './Loader';
|
||||
|
||||
import s from './Loader.module.pcss';
|
||||
|
||||
export const PageLoader = () => <Loader overlayClassName={s.pageOverlay} className={s.pageLoader} icon="loader" />;
|
||||
@ -1,2 +1,3 @@
|
||||
export { InlineLoader } from './InlineLoader';
|
||||
export { Loader } from './Loader';
|
||||
export { PageLoader } from './PageLoader';
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { SwitchGroup } from '../SettingsGroup';
|
||||
import { RoutePath } from 'panel/components/Routes/Paths';
|
||||
import { Link } from 'panel/common/ui/Link';
|
||||
|
||||
import { SwitchGroup } from './SettingsGroup';
|
||||
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
@ -5,11 +5,22 @@
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
cursor: pointer;
|
||||
color: var(--default-gray-icons);
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
padding: 16px;
|
||||
max-width: 240px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownTitle {
|
||||
|
||||
@ -2,28 +2,15 @@ import React, { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { toNumber } from 'panel/helpers/form';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { QUERY_LOG_INTERVALS_DAYS, DAY, RETENTION_CUSTOM } from 'panel/helpers/constants';
|
||||
|
||||
import { QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
|
||||
import { RadioGroup } from '../SettingsGroup/RadioGroup';
|
||||
import { SwitchGroup } from '../SettingsGroup';
|
||||
import { IgnoredDomains } from '../IgnoredDomains';
|
||||
|
||||
const getIntervalTitle = (intervalDays: number) => {
|
||||
switch (intervalDays) {
|
||||
case RETENTION_CUSTOM:
|
||||
return intl.getMessage('settings_custom');
|
||||
case 6 * HOUR:
|
||||
return intl.getPlural('settings_hours', 6);
|
||||
case DAY:
|
||||
return intl.getPlural('settings_hours', 24);
|
||||
default:
|
||||
return intl.getPlural('settings_days', intervalDays / DAY);
|
||||
}
|
||||
};
|
||||
import { getIntervalTitle } from '../helpers';
|
||||
import { RetentionCustomInput } from '../RetentionCustomInput';
|
||||
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
@ -121,27 +108,14 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
||||
value: interval,
|
||||
})),
|
||||
]}>
|
||||
<Controller
|
||||
name="customInterval"
|
||||
<RetentionCustomInput
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className={theme.form.input}>
|
||||
<Input
|
||||
id="logs_config_custom_interval"
|
||||
label={intl.getMessage('settings_log_rotation_hours')}
|
||||
placeholder={intl.getMessage('settings_rotation_placeholder')}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
disabled={processing || QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}
|
||||
errorMessage={fieldState.error?.message}
|
||||
min={RETENTION_RANGE.MIN}
|
||||
max={RETENTION_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
processing={processing}
|
||||
intervalValue={intervalValue}
|
||||
intervals={QUERY_LOG_INTERVALS_DAYS}
|
||||
inputId="logs_config_custom_interval"
|
||||
inputLabel={intl.getMessage('settings_log_rotation_hours')}
|
||||
placeholder={intl.getMessage('settings_rotation_placeholder')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
|
||||
66
client_v2/src/components/Settings/RetentionCustomInput.tsx
Normal file
66
client_v2/src/components/Settings/RetentionCustomInput.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Controller, type Control, type Path } from 'react-hook-form';
|
||||
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { RETENTION_CUSTOM, RETENTION_RANGE } from 'panel/helpers/constants';
|
||||
import { toNumber } from 'panel/helpers/form';
|
||||
|
||||
type Props<TFormValues extends { customInterval?: number | null }> = {
|
||||
control: Control<TFormValues>;
|
||||
processing: boolean;
|
||||
intervalValue: number;
|
||||
intervals: number[];
|
||||
inputId: string;
|
||||
inputLabel: string;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export const RetentionCustomInput = <TFormValues extends { customInterval?: number | null }>({
|
||||
control,
|
||||
processing,
|
||||
intervalValue,
|
||||
intervals,
|
||||
inputId,
|
||||
inputLabel,
|
||||
placeholder,
|
||||
}: Props<TFormValues>) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (intervalValue === RETENTION_CUSTOM && !processing) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [intervalValue, processing]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={'customInterval' as Path<TFormValues>}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className={theme.form.input}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={inputId}
|
||||
label={inputLabel}
|
||||
placeholder={placeholder}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
disabled={processing || intervals.includes(intervalValue)}
|
||||
error={
|
||||
intervalValue === RETENTION_CUSTOM &&
|
||||
fieldState.isTouched &&
|
||||
String(field.value ?? '').trim() === ''
|
||||
}
|
||||
min={RETENTION_RANGE.MIN}
|
||||
max={RETENTION_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -3,41 +3,32 @@ import cn from 'clsx';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { Checkbox } from 'panel/common/controls/Checkbox';
|
||||
import { Loader } from 'panel/common/ui/Loader';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { PageLoader } from 'panel/common/ui/Loader';
|
||||
import { SettingsData, StatsData, QueryLogsData, FilteringData } from 'panel/initialState';
|
||||
|
||||
import { StatsConfig } from './StatsConfig/StatsConfig';
|
||||
import { LogsConfig } from './LogsConfig';
|
||||
import { FiltersConfig } from './FiltersConfig';
|
||||
import { getObjectKeysSorted, captitalizeWords } from '../../helpers/helpers';
|
||||
import { SettingsData, StatsData, QueryLogsData, FilteringData } from '../../initialState';
|
||||
import { getSafeSearchProviderTitle } from './helpers';
|
||||
import type { StatsConfigPayload } from './StatsConfig/StatsConfig';
|
||||
import type { LogsConfigPayload } from './LogsConfig/LogsConfig';
|
||||
import type { FormValues as FiltersFormValues } from './FiltersConfig';
|
||||
import { SwitchGroup } from './SettingsGroup';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
const ORDER_KEY = 'order';
|
||||
|
||||
const SETTINGS = {
|
||||
safebrowsing: {
|
||||
enabled: false,
|
||||
title: intl.getMessage('settings_browsing_security'),
|
||||
subtitle: intl.getMessage('settings_browsing_security_desc'),
|
||||
testId: 'safebrowsing',
|
||||
[ORDER_KEY]: 0,
|
||||
},
|
||||
parental: {
|
||||
enabled: false,
|
||||
title: intl.getMessage('settings_parental_control'),
|
||||
subtitle: intl.getMessage('settings_parental_control_desc'),
|
||||
testId: 'parental',
|
||||
[ORDER_KEY]: 1,
|
||||
},
|
||||
};
|
||||
|
||||
type InitSettingsArg = typeof SETTINGS;
|
||||
type ToggleSettingArgKey = keyof typeof SETTINGS | 'safesearch';
|
||||
type ToggleSettingArgValue = boolean | Record<string, boolean>;
|
||||
|
||||
@ -46,16 +37,16 @@ type Props = {
|
||||
stats: StatsData;
|
||||
queryLogs: QueryLogsData;
|
||||
filtering: FilteringData;
|
||||
initSettings: (settings: InitSettingsArg) => void;
|
||||
initSettings: () => void;
|
||||
toggleSetting: (key: ToggleSettingArgKey, value: ToggleSettingArgValue) => void;
|
||||
getStatsConfig: () => void;
|
||||
setStatsConfig: (config: StatsConfigPayload) => void;
|
||||
resetStats: () => void;
|
||||
setFiltersConfig: (values: FiltersFormValues) => void;
|
||||
getFilteringStatus: () => void;
|
||||
getLogsConfig?: () => void;
|
||||
setLogsConfig?: (values: LogsConfigPayload) => void;
|
||||
clearLogs?: () => void;
|
||||
getLogsConfig: () => void;
|
||||
setLogsConfig: (values: LogsConfigPayload) => void;
|
||||
clearLogs: () => void;
|
||||
};
|
||||
|
||||
export const Settings = ({
|
||||
@ -75,21 +66,17 @@ export const Settings = ({
|
||||
setFiltersConfig,
|
||||
}: Props) => {
|
||||
useEffect(() => {
|
||||
initSettings(SETTINGS);
|
||||
initSettings();
|
||||
getStatsConfig();
|
||||
getFilteringStatus();
|
||||
|
||||
if (getLogsConfig) {
|
||||
getLogsConfig();
|
||||
}
|
||||
getLogsConfig();
|
||||
}, []);
|
||||
|
||||
const renderSettings = (settingsList?: SettingsData['settingsList']) =>
|
||||
settingsList
|
||||
? getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: keyof typeof SETTINGS) => {
|
||||
const setting = settingsList[key];
|
||||
const { enabled, title, subtitle } = setting;
|
||||
|
||||
? (Object.keys(SETTINGS) as Array<keyof typeof SETTINGS>).map((key) => {
|
||||
const { title, subtitle } = SETTINGS[key];
|
||||
const enabled = Boolean(settingsList[key]?.enabled);
|
||||
return (
|
||||
<div key={key}>
|
||||
<SwitchGroup
|
||||
@ -97,7 +84,7 @@ export const Settings = ({
|
||||
description={subtitle}
|
||||
id={String(key)}
|
||||
checked={enabled}
|
||||
onChange={(checked) => toggleSetting(key as ToggleSettingArgKey, !checked)}
|
||||
onChange={(e) => toggleSetting(key, !e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -122,7 +109,7 @@ export const Settings = ({
|
||||
onChange={(e) => toggleSetting('safesearch', { ...safesearch, enabled: e.target.checked })}>
|
||||
<div>
|
||||
{Object.keys(searches).map((searchKey) => (
|
||||
<div key={searchKey} className={s.checkbox}>
|
||||
<div key={searchKey} className={theme.form.checkbox}>
|
||||
<Checkbox
|
||||
id={searchKey}
|
||||
checked={searches[searchKey]}
|
||||
@ -130,7 +117,7 @@ export const Settings = ({
|
||||
onChange={(e) => {
|
||||
toggleSetting('safesearch', { ...safesearch, [searchKey]: e.target.checked });
|
||||
}}>
|
||||
{captitalizeWords(searchKey)}
|
||||
{getSafeSearchProviderTitle(searchKey)}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
@ -139,7 +126,7 @@ export const Settings = ({
|
||||
);
|
||||
};
|
||||
|
||||
const isDataReady = !settings.processing && !stats.processingGetConfig && !queryLogs.processingGetConfig;
|
||||
const isLoading = settings.processing || stats.processingGetConfig || queryLogs.processingGetConfig;
|
||||
|
||||
return (
|
||||
<div className={theme.layout.container}>
|
||||
@ -147,9 +134,9 @@ export const Settings = ({
|
||||
{intl.getMessage('general_settings')}
|
||||
</h1>
|
||||
|
||||
{!isDataReady && <Loader />}
|
||||
{isLoading && <PageLoader />}
|
||||
|
||||
{isDataReady && (
|
||||
{!isLoading && (
|
||||
<>
|
||||
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('settings_filter_requests')}
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
@ -34,8 +35,8 @@ export const RadioGroup = <T extends number | string | boolean>({
|
||||
<div className={cn(s.switch, className)}>
|
||||
<div className={s.row}>
|
||||
<div className={s.text}>
|
||||
<div className={s.title}>{title}</div>
|
||||
{description && <div className={s.desc}>{description}</div>}
|
||||
<div className={cn(s.title, theme.text.t2, theme.text.semibold)}>{title}</div>
|
||||
{description && <div className={cn(s.desc, theme.text.t3)}>{description}</div>}
|
||||
</div>
|
||||
<div className={s.input} />
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { ReactNode, useRef } from 'react';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { Switch } from 'panel/common/controls/Switch';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
@ -19,7 +20,7 @@ type Props = {
|
||||
export const SwitchGroup = ({ title, description, id, className, checked, onChange, disabled, children }: Props) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleRowClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleRowClick = () => {
|
||||
if (disabled || !inputRef.current) {
|
||||
return;
|
||||
}
|
||||
@ -31,8 +32,8 @@ export const SwitchGroup = ({ title, description, id, className, checked, onChan
|
||||
<div className={cn(s.switch, className)}>
|
||||
<div className={s.row} onClick={handleRowClick}>
|
||||
<div className={s.text}>
|
||||
<div className={s.title}>{title}</div>
|
||||
{description && <div className={s.desc}>{description}</div>}
|
||||
<div className={cn(theme.text.t2, theme.text.semibold, s.title)}>{title}</div>
|
||||
{description && <div className={cn(theme.text.t3, s.desc)}>{description}</div>}
|
||||
</div>
|
||||
<div className={s.input} onClick={(e) => e.stopPropagation()}>
|
||||
<Switch id={id} checked={checked} onChange={onChange} disabled={disabled} ref={inputRef} />
|
||||
|
||||
@ -27,14 +27,10 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: var(--weight-semi-bold);
|
||||
line-height: 24px;
|
||||
color: var(--default-main-text);
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: var(--default-description-text);
|
||||
}
|
||||
|
||||
|
||||
@ -1,27 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import i18next from 'i18next';
|
||||
import { getIntervalTitle } from '../helpers';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import intl from 'panel/common/intl';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
|
||||
import { toNumber } from '../../../helpers/form';
|
||||
import { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM } from '../../../helpers/constants';
|
||||
import { RadioGroup, SwitchGroup } from '../SettingsGroup';
|
||||
import { IgnoredDomains } from '../IgnoredDomains';
|
||||
|
||||
const getIntervalTitle = (interval: any) => {
|
||||
switch (interval) {
|
||||
case RETENTION_CUSTOM:
|
||||
return i18next.t('settings_custom');
|
||||
case DAY:
|
||||
return i18next.t('interval_24_hour');
|
||||
default:
|
||||
return i18next.t('interval_days', { count: interval / DAY });
|
||||
}
|
||||
};
|
||||
import { RetentionCustomInput } from '../RetentionCustomInput';
|
||||
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
@ -31,14 +19,6 @@ export type FormValues = {
|
||||
ignore_enabled: boolean;
|
||||
};
|
||||
|
||||
const defaultFormValues = {
|
||||
enabled: false,
|
||||
interval: DAY,
|
||||
customInterval: null,
|
||||
ignored: '',
|
||||
ignore_enabled: false,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialValues: FormValues;
|
||||
processing: boolean;
|
||||
@ -57,8 +37,11 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
||||
} = useForm<FormValues>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
...defaultFormValues,
|
||||
...initialValues,
|
||||
enabled: initialValues.enabled || false,
|
||||
interval: initialValues.interval || DAY,
|
||||
customInterval: initialValues.customInterval ?? undefined,
|
||||
ignored: initialValues.ignored || '',
|
||||
ignore_enabled: initialValues.ignore_enabled || true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -72,6 +55,8 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
||||
}
|
||||
}, [intervalValue]);
|
||||
|
||||
// Focus is handled inside RetentionCustomInput
|
||||
|
||||
const onSubmitForm = (data: FormValues) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
@ -108,27 +93,14 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
|
||||
value: interval,
|
||||
})),
|
||||
]}>
|
||||
<Controller
|
||||
name="customInterval"
|
||||
<RetentionCustomInput
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className={theme.form.input}>
|
||||
<Input
|
||||
id="stats_config_custom_interval"
|
||||
label={intl.getMessage('settings_statistics_retention_hours')}
|
||||
placeholder={intl.getMessage('settings_rotation_placeholder')}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
disabled={processing || STATS_INTERVALS_DAYS.includes(intervalValue)}
|
||||
errorMessage={fieldState.error?.message}
|
||||
min={RETENTION_RANGE.MIN}
|
||||
max={RETENTION_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
processing={processing}
|
||||
intervalValue={intervalValue}
|
||||
intervals={STATS_INTERVALS_DAYS}
|
||||
inputId="stats_config_custom_interval"
|
||||
inputLabel={intl.getMessage('settings_statistics_retention_hours')}
|
||||
placeholder={intl.getMessage('settings_rotation_placeholder')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Form, FormValues } from './Form';
|
||||
import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import intl from 'panel/common/intl';
|
||||
import { HOUR } from 'panel/helpers/constants';
|
||||
import { formatIntervalText } from 'panel/components/Settings/helpers';
|
||||
|
||||
import { Form, FormValues } from './Form';
|
||||
|
||||
export type StatsConfigPayload = {
|
||||
enabled: boolean;
|
||||
ignored: string[];
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import intl from 'panel/common/intl';
|
||||
import { HOUR, DAY } from 'panel/helpers/constants';
|
||||
import { HOUR, DAY, RETENTION_CUSTOM } from 'panel/helpers/constants';
|
||||
import { captitalizeWords } from '../../helpers/helpers';
|
||||
|
||||
export const formatIntervalText = (intervalMs: number) => {
|
||||
if (intervalMs === 6 * HOUR) {
|
||||
@ -13,3 +14,24 @@ export const formatIntervalText = (intervalMs: number) => {
|
||||
}
|
||||
return intl.getPlural('settings_hours', Math.floor(intervalMs / HOUR));
|
||||
};
|
||||
|
||||
export const getIntervalTitle = (intervalMs: number) => {
|
||||
if (intervalMs === RETENTION_CUSTOM) {
|
||||
return intl.getMessage('settings_custom');
|
||||
}
|
||||
return formatIntervalText(intervalMs);
|
||||
};
|
||||
|
||||
const SAFESEARCH_TITLES: Record<string, string> = {
|
||||
bing: 'Bing',
|
||||
duckduckgo: 'DuckDuckGo',
|
||||
ecosia: 'Ecosia',
|
||||
google: 'Google',
|
||||
pixabay: 'Pixabay',
|
||||
yandex: 'Yandex',
|
||||
youtube: 'YouTube',
|
||||
};
|
||||
|
||||
export const getSafeSearchProviderTitle = (key: string) => {
|
||||
return SAFESEARCH_TITLES[key] ?? captitalizeWords(key);
|
||||
};
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
}
|
||||
@ -8,46 +8,7 @@
|
||||
|
||||
@media (min-width: 768px) {
|
||||
right: 24px;
|
||||
width: 345px;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 16px;
|
||||
font-weight: var(--weight-regular);
|
||||
color: var(--default-main-text);
|
||||
background-color: var(--default-page-background);
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
2px 2px 4px 0 rgba(0, 0, 0, 0.1),
|
||||
4px 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 12px 0 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&_success {
|
||||
color: var(--default-product-icon);
|
||||
}
|
||||
|
||||
&_error {
|
||||
color: var(--default-error-icon);
|
||||
}
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { TOAST_TIMEOUTS } from '../../helpers/constants';
|
||||
|
||||
import { removeToast } from '../../actions';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import s from './styles.module.pcss';
|
||||
import cn from 'clsx';
|
||||
|
||||
interface ToastProps {
|
||||
id: string;
|
||||
message: string;
|
||||
type: string;
|
||||
options?: object;
|
||||
}
|
||||
|
||||
const Toast = ({ id, message, type, options }: ToastProps) => {
|
||||
const Toast = ({ id, message, type }: ToastProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [timerId, setTimerId] = useState(null);
|
||||
|
||||
@ -31,12 +31,10 @@ const Toast = ({ id, message, type, options }: ToastProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="toast" onMouseOver={clearRemoveToastTimeout} onMouseOut={setRemoveToastTimeout}>
|
||||
<Icon icon={type === 'success' ? 'check' : 'cross'} className={`toast__icon toast__icon_${type}`} />
|
||||
<div className={s.toast} onMouseOver={clearRemoveToastTimeout} onMouseOut={setRemoveToastTimeout}>
|
||||
<Icon icon={type === 'success' ? 'check' : 'attention'} className={cn(s.icon, s[type])} />
|
||||
|
||||
<div className="toast__content">
|
||||
<Trans i18nKey={message} {...options} />
|
||||
</div>
|
||||
<div className={s.content}>{message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
38
client_v2/src/components/Toasts/styles.module.pcss
Normal file
38
client_v2/src/components/Toasts/styles.module.pcss
Normal file
@ -0,0 +1,38 @@
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 16px;
|
||||
font-weight: var(--weight-regular);
|
||||
color: var(--default-main-text);
|
||||
background-color: var(--default-page-background);
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
2px 2px 4px 0 rgba(0, 0, 0, 0.1),
|
||||
4px 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 12px 0 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&.success {
|
||||
color: var(--default-product-icon);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--default-error-icon);
|
||||
}
|
||||
}
|
||||
@ -148,15 +148,9 @@ export type SettingsData = {
|
||||
settingsList?: {
|
||||
parental: {
|
||||
enabled: boolean;
|
||||
order: number;
|
||||
subtitle: string;
|
||||
title: string;
|
||||
};
|
||||
safebrowsing: {
|
||||
enabled: boolean;
|
||||
order: number;
|
||||
subtitle: string;
|
||||
title: string;
|
||||
};
|
||||
safesearch: Record<string, boolean>;
|
||||
};
|
||||
|
||||
@ -1,20 +1,30 @@
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px 16px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
.container {
|
||||
margin: 0 16px;
|
||||
padding: 40px 0;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
color: var(--default-main-text);
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin: 0;
|
||||
padding: 48px 24px 72px;
|
||||
max-width: 632px;
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ const meta: Meta<typeof Switch> = {
|
||||
control: 'text',
|
||||
description: 'Additional CSS class for the wrapper element',
|
||||
},
|
||||
handleChange: { action: 'changed' },
|
||||
onChange: { action: 'changed' },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user