diff --git a/client_v2/src/actions/index.tsx b/client_v2/src/actions/index.tsx index 7ca4fd2a..677cbde3 100644 --- a/client_v2/src/actions/index.tsx +++ b/client_v2/src/actions/index.tsx @@ -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 & { enabled: boolean }; type ToggleSettingKey = keyof typeof SETTINGS_NAMES; -type SettingsItem = NonNullable['safebrowsing']; -type InitSettingsItemInput = Partial> & - Record; -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 & { enabled: boolean }; - parental: Record & { 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'); diff --git a/client_v2/src/actions/queryLogs.ts b/client_v2/src/actions/queryLogs.ts index e148dadb..6b0688d9 100644 --- a/client_v2/src/actions/queryLogs.ts +++ b/client_v2/src/actions/queryLogs.ts @@ -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 })); diff --git a/client_v2/src/actions/stats.ts b/client_v2/src/actions/stats.ts index 7987360a..f7caa3a6 100644 --- a/client_v2/src/actions/stats.ts +++ b/client_v2/src/actions/stats.ts @@ -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 })); diff --git a/client_v2/src/common/ui/Header/Header.module.pcss b/client_v2/src/common/ui/Header/Header.module.pcss index ff77d11a..75f71ec3 100644 --- a/client_v2/src/common/ui/Header/Header.module.pcss +++ b/client_v2/src/common/ui/Header/Header.module.pcss @@ -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; } } diff --git a/client_v2/src/common/ui/Icons/index.tsx b/client_v2/src/common/ui/Icons/index.tsx index 900b7737..5d7b1a86 100644 --- a/client_v2/src/common/ui/Icons/index.tsx +++ b/client_v2/src/common/ui/Icons/index.tsx @@ -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" /> + + + + + + )); Icons.displayName = 'Icons'; diff --git a/client_v2/src/common/ui/Loader/Loader.module.pcss b/client_v2/src/common/ui/Loader/Loader.module.pcss index ea924eb2..3c7147f4 100644 --- a/client_v2/src/common/ui/Loader/Loader.module.pcss +++ b/client_v2/src/common/ui/Loader/Loader.module.pcss @@ -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); +} diff --git a/client_v2/src/common/ui/Loader/ModalLoader.tsx b/client_v2/src/common/ui/Loader/ModalLoader.tsx deleted file mode 100644 index f677be90..00000000 --- a/client_v2/src/common/ui/Loader/ModalLoader.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Loader } from './Loader'; - -import s from './Loader.module.pcss'; - -export const ModalLoader = () => ; diff --git a/client_v2/src/common/ui/Loader/PageLoader.tsx b/client_v2/src/common/ui/Loader/PageLoader.tsx new file mode 100644 index 00000000..1cb9721c --- /dev/null +++ b/client_v2/src/common/ui/Loader/PageLoader.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Loader } from './Loader'; + +import s from './Loader.module.pcss'; + +export const PageLoader = () => ; diff --git a/client_v2/src/common/ui/Loader/index.ts b/client_v2/src/common/ui/Loader/index.ts index 82b7582e..01a1822e 100644 --- a/client_v2/src/common/ui/Loader/index.ts +++ b/client_v2/src/common/ui/Loader/index.ts @@ -1,2 +1,3 @@ export { InlineLoader } from './InlineLoader'; export { Loader } from './Loader'; +export { PageLoader } from './PageLoader'; diff --git a/client_v2/src/components/Settings/FiltersConfig/index.tsx b/client_v2/src/components/Settings/FiltersConfig.tsx similarity index 96% rename from client_v2/src/components/Settings/FiltersConfig/index.tsx rename to client_v2/src/components/Settings/FiltersConfig.tsx index be79c900..878ee6e6 100644 --- a/client_v2/src/components/Settings/FiltersConfig/index.tsx +++ b/client_v2/src/components/Settings/FiltersConfig.tsx @@ -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; diff --git a/client_v2/src/components/Settings/IgnoredDomains/styles.module.pcss b/client_v2/src/components/Settings/IgnoredDomains/styles.module.pcss index 944b058b..56574ef7 100644 --- a/client_v2/src/components/Settings/IgnoredDomains/styles.module.pcss +++ b/client_v2/src/components/Settings/IgnoredDomains/styles.module.pcss @@ -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 { diff --git a/client_v2/src/components/Settings/LogsConfig/Form.tsx b/client_v2/src/components/Settings/LogsConfig/Form.tsx index 44399b9a..024e0b42 100644 --- a/client_v2/src/components/Settings/LogsConfig/Form.tsx +++ b/client_v2/src/components/Settings/LogsConfig/Form.tsx @@ -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, })), ]}> - ( -
- { - 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} - /> -
- )} + 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')} /> diff --git a/client_v2/src/components/Settings/RetentionCustomInput.tsx b/client_v2/src/components/Settings/RetentionCustomInput.tsx new file mode 100644 index 00000000..07e0b96d --- /dev/null +++ b/client_v2/src/components/Settings/RetentionCustomInput.tsx @@ -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 = { + control: Control; + processing: boolean; + intervalValue: number; + intervals: number[]; + inputId: string; + inputLabel: string; + placeholder: string; +}; + +export const RetentionCustomInput = ({ + control, + processing, + intervalValue, + intervals, + inputId, + inputLabel, + placeholder, +}: Props) => { + const inputRef = useRef(null); + + useEffect(() => { + if (intervalValue === RETENTION_CUSTOM && !processing) { + inputRef.current?.focus(); + } + }, [intervalValue, processing]); + + return ( + } + control={control} + render={({ field, fieldState }) => ( +
+ { + 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} + /> +
+ )} + /> + ); +}; diff --git a/client_v2/src/components/Settings/Settings.tsx b/client_v2/src/components/Settings/Settings.tsx index ca170a08..e2280034 100644 --- a/client_v2/src/components/Settings/Settings.tsx +++ b/client_v2/src/components/Settings/Settings.tsx @@ -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; @@ -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).map((key) => { + const { title, subtitle } = SETTINGS[key]; + const enabled = Boolean(settingsList[key]?.enabled); return (
toggleSetting(key as ToggleSettingArgKey, !checked)} + onChange={(e) => toggleSetting(key, !e.target.checked)} />
); @@ -122,7 +109,7 @@ export const Settings = ({ onChange={(e) => toggleSetting('safesearch', { ...safesearch, enabled: e.target.checked })}>
{Object.keys(searches).map((searchKey) => ( -
+
{ toggleSetting('safesearch', { ...safesearch, [searchKey]: e.target.checked }); }}> - {captitalizeWords(searchKey)} + {getSafeSearchProviderTitle(searchKey)}
))} @@ -139,7 +126,7 @@ export const Settings = ({ ); }; - const isDataReady = !settings.processing && !stats.processingGetConfig && !queryLogs.processingGetConfig; + const isLoading = settings.processing || stats.processingGetConfig || queryLogs.processingGetConfig; return (
@@ -147,9 +134,9 @@ export const Settings = ({ {intl.getMessage('general_settings')} - {!isDataReady && } + {isLoading && } - {isDataReady && ( + {!isLoading && ( <>

{intl.getMessage('settings_filter_requests')} diff --git a/client_v2/src/components/Settings/SettingsGroup/RadioGroup.tsx b/client_v2/src/components/Settings/SettingsGroup/RadioGroup.tsx index fb8604bf..2c8f6520 100644 --- a/client_v2/src/components/Settings/SettingsGroup/RadioGroup.tsx +++ b/client_v2/src/components/Settings/SettingsGroup/RadioGroup.tsx @@ -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 = ({
-
{title}
- {description &&
{description}
} +
{title}
+ {description &&
{description}
}
diff --git a/client_v2/src/components/Settings/SettingsGroup/SwitchGroup.tsx b/client_v2/src/components/Settings/SettingsGroup/SwitchGroup.tsx index 294dc819..56a3350f 100644 --- a/client_v2/src/components/Settings/SettingsGroup/SwitchGroup.tsx +++ b/client_v2/src/components/Settings/SettingsGroup/SwitchGroup.tsx @@ -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(null); - const handleRowClick = (e: React.MouseEvent) => { + const handleRowClick = () => { if (disabled || !inputRef.current) { return; } @@ -31,8 +32,8 @@ export const SwitchGroup = ({ title, description, id, className, checked, onChan
-
{title}
- {description &&
{description}
} +
{title}
+ {description &&
{description}
}
e.stopPropagation()}> diff --git a/client_v2/src/components/Settings/SettingsGroup/styles.module.pcss b/client_v2/src/components/Settings/SettingsGroup/styles.module.pcss index 58bb4f0f..9739abd8 100644 --- a/client_v2/src/components/Settings/SettingsGroup/styles.module.pcss +++ b/client_v2/src/components/Settings/SettingsGroup/styles.module.pcss @@ -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); } diff --git a/client_v2/src/components/Settings/StatsConfig/Form.tsx b/client_v2/src/components/Settings/StatsConfig/Form.tsx index a28a34d3..143d21d3 100644 --- a/client_v2/src/components/Settings/StatsConfig/Form.tsx +++ b/client_v2/src/components/Settings/StatsConfig/Form.tsx @@ -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({ 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, })), ]}> - ( -
- { - 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} - /> -
- )} + 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')} /> diff --git a/client_v2/src/components/Settings/StatsConfig/StatsConfig.tsx b/client_v2/src/components/Settings/StatsConfig/StatsConfig.tsx index 0afc383d..836b7daf 100644 --- a/client_v2/src/components/Settings/StatsConfig/StatsConfig.tsx +++ b/client_v2/src/components/Settings/StatsConfig/StatsConfig.tsx @@ -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[]; diff --git a/client_v2/src/components/Settings/helpers.ts b/client_v2/src/components/Settings/helpers.ts index 8dbeb571..c9b05433 100644 --- a/client_v2/src/components/Settings/helpers.ts +++ b/client_v2/src/components/Settings/helpers.ts @@ -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 = { + 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); +}; diff --git a/client_v2/src/components/Settings/styles.module.pcss b/client_v2/src/components/Settings/styles.module.pcss deleted file mode 100644 index 029632f0..00000000 --- a/client_v2/src/components/Settings/styles.module.pcss +++ /dev/null @@ -1,8 +0,0 @@ -.checkbox { - display: flex; - align-items: center; - padding: 8px 0; -} - -.buttons { -} diff --git a/client_v2/src/components/Toasts/Toast.pcss b/client_v2/src/components/Toasts/Toast.pcss index b485c3ad..7b6cd4ed 100644 --- a/client_v2/src/components/Toasts/Toast.pcss +++ b/client_v2/src/components/Toasts/Toast.pcss @@ -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; } } diff --git a/client_v2/src/components/Toasts/Toast.tsx b/client_v2/src/components/Toasts/Toast.tsx index c28fa082..0991c13c 100644 --- a/client_v2/src/components/Toasts/Toast.tsx +++ b/client_v2/src/components/Toasts/Toast.tsx @@ -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 ( -
- +
+ -
- -
+
{message}
); }; diff --git a/client_v2/src/components/Toasts/styles.module.pcss b/client_v2/src/components/Toasts/styles.module.pcss new file mode 100644 index 00000000..24fd3352 --- /dev/null +++ b/client_v2/src/components/Toasts/styles.module.pcss @@ -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); + } +} diff --git a/client_v2/src/initialState.ts b/client_v2/src/initialState.ts index 7c32c046..f8abbb22 100644 --- a/client_v2/src/initialState.ts +++ b/client_v2/src/initialState.ts @@ -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; }; diff --git a/client_v2/src/lib/theme/Form.module.pcss b/client_v2/src/lib/theme/Form.module.pcss index 1814dde6..c91e546a 100644 --- a/client_v2/src/lib/theme/Form.module.pcss +++ b/client_v2/src/lib/theme/Form.module.pcss @@ -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; +} diff --git a/client_v2/src/lib/theme/Layout.module.pcss b/client_v2/src/lib/theme/Layout.module.pcss index 6c2c7a18..c0d0d3bf 100644 --- a/client_v2/src/lib/theme/Layout.module.pcss +++ b/client_v2/src/lib/theme/Layout.module.pcss @@ -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; } diff --git a/client_v2/src/stories/controls/Switch.stories.tsx b/client_v2/src/stories/controls/Switch.stories.tsx index 1ca206a1..6fc473e6 100644 --- a/client_v2/src/stories/controls/Switch.stories.tsx +++ b/client_v2/src/stories/controls/Switch.stories.tsx @@ -49,7 +49,7 @@ const meta: Meta = { control: 'text', description: 'Additional CSS class for the wrapper element', }, - handleChange: { action: 'changed' }, + onChange: { action: 'changed' }, }, };