ADG-10293

This commit is contained in:
Ildar Kamalov 2025-08-09 21:00:55 +03:00
parent b3ca6c9934
commit f7357be494
28 changed files with 293 additions and 248 deletions

View File

@ -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');

View File

@ -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 }));

View File

@ -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 }));

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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);
}

View File

@ -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" />;

View 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" />;

View File

@ -1,2 +1,3 @@
export { InlineLoader } from './InlineLoader';
export { Loader } from './Loader';
export { PageLoader } from './PageLoader';

View File

@ -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;

View File

@ -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 {

View File

@ -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>

View 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>
)}
/>
);
};

View File

@ -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')}

View File

@ -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>

View File

@ -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} />

View File

@ -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);
}

View File

@ -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>

View File

@ -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[];

View File

@ -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);
};

View File

@ -1,8 +0,0 @@
.checkbox {
display: flex;
align-items: center;
padding: 8px 0;
}
.buttons {
}

View File

@ -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;
}
}

View File

@ -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>
);
};

View 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);
}
}

View File

@ -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>;
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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' },
},
};