mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
feat: replace notification checkboxes with card-based layout (#5421)
Replace EventBusCheckboxes with card-based notification settings: - Each event group gets its own card with responsive grid layout - Master checkbox per group with indeterminate state - Inline parameter inputs (CPU threshold) appear when enabled - Theme-adaptive via Ant Design Card component Components: - NotificationLayout, NotificationCard, NotificationHeader, NotificationEvent - TelegramNotifications, EmailNotifications with explicit event configs
This commit is contained in:
@@ -1,147 +0,0 @@
|
||||
import { Checkbox, Collapse, InputNumber, Space } from 'antd';
|
||||
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface EventGroup {
|
||||
key: string;
|
||||
labelKey: string;
|
||||
events: { value: string; labelKey: string }[];
|
||||
}
|
||||
|
||||
const EVENT_GROUPS: EventGroup[] = [
|
||||
{
|
||||
key: 'outbound',
|
||||
labelKey: 'pages.settings.eventGroupOutbound',
|
||||
events: [
|
||||
{ value: 'outbound.down', labelKey: 'pages.settings.eventOutboundDown' },
|
||||
{ value: 'outbound.up', labelKey: 'pages.settings.eventOutboundUp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'xray',
|
||||
labelKey: 'pages.settings.eventGroupXray',
|
||||
events: [
|
||||
{ value: 'xray.crash', labelKey: 'pages.settings.eventXrayCrash' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'node',
|
||||
labelKey: 'pages.settings.eventGroupNode',
|
||||
events: [
|
||||
{ value: 'node.down', labelKey: 'pages.settings.eventNodeDown' },
|
||||
{ value: 'node.up', labelKey: 'pages.settings.eventNodeUp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
labelKey: 'pages.settings.eventGroupSystem',
|
||||
events: [
|
||||
{ value: 'cpu.high', labelKey: 'pages.settings.eventCPUHigh' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'security',
|
||||
labelKey: 'pages.settings.eventGroupSecurity',
|
||||
events: [
|
||||
{ value: 'login.attempt', labelKey: 'pages.settings.eventLoginAttempt' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface EventBusCheckboxesProps {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
/** Maps event value → { key: setting field name, value: current value } for inline inputs */
|
||||
extra?: Record<string, { key: string; value: number }>;
|
||||
/** Callback when extra input changes: (settingKey, newValue) => void */
|
||||
onExtraChange?: (key: string, v: number | null) => void;
|
||||
}
|
||||
|
||||
export function EventBusCheckboxes({ value, onChange, extra, onExtraChange }: EventBusCheckboxesProps) {
|
||||
const { t } = useTranslation();
|
||||
const selected = value ? value.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||
|
||||
function toggle(eventType: string) {
|
||||
const next = selected.includes(eventType)
|
||||
? selected.filter((e) => e !== eventType)
|
||||
: [...selected, eventType];
|
||||
onChange(next.join(','));
|
||||
}
|
||||
|
||||
function toggleGroup(group: EventGroup) {
|
||||
const groupValues = group.events.map((e) => e.value);
|
||||
const allSelected = groupValues.every((v) => selected.includes(v));
|
||||
let next: string[];
|
||||
if (allSelected) {
|
||||
next = selected.filter((v) => !groupValues.includes(v));
|
||||
} else {
|
||||
next = [...new Set([...selected, ...groupValues])];
|
||||
}
|
||||
onChange(next.join(','));
|
||||
}
|
||||
|
||||
const items = EVENT_GROUPS.map((group) => {
|
||||
const count = group.events.filter((e) => selected.includes(e.value)).length;
|
||||
const total = group.events.length;
|
||||
const allSelected = count === total;
|
||||
|
||||
return {
|
||||
key: group.key,
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontWeight: 500 }}>{t(group.labelKey)}</span>
|
||||
<span style={{ color: '#999', fontSize: 12 }}>
|
||||
{count}/{total}
|
||||
</span>
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
indeterminate={count > 0 && count < total}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={() => toggleGroup(group)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<Checkbox.Group value={selected} style={{ width: '100%' }}>
|
||||
<Space wrap size={[16, 4]}>
|
||||
{group.events.map((et) => {
|
||||
const checked = selected.includes(et.value);
|
||||
const extraConf = extra?.[et.value];
|
||||
return (
|
||||
<span key={et.value} style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<Checkbox value={et.value} onChange={() => toggle(et.value)}>
|
||||
{t(et.labelKey)}
|
||||
</Checkbox>
|
||||
{extraConf && onExtraChange && (
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
value={extraConf.value}
|
||||
disabled={!checked}
|
||||
onChange={(v) => onExtraChange(extraConf.key, v)}
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const defaultActiveKeys = EVENT_GROUPS
|
||||
.filter((g) => g.events.some((e) => selected.includes(e.value)))
|
||||
.map((g) => g.key);
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
items={items}
|
||||
defaultActiveKey={defaultActiveKeys.length > 0 ? defaultActiveKeys : ['outbound']}
|
||||
expandIcon={({ isActive }) => isActive ? <DownOutlined /> : <RightOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export { default as InputAddon } from './InputAddon';
|
||||
export { default as InfinityIcon } from './InfinityIcon';
|
||||
export { default as SettingListItem } from './SettingListItem';
|
||||
export { EventBusCheckboxes } from './EventBusCheckboxes';
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { InputNumber } from 'antd';
|
||||
import { CloudServerOutlined, ThunderboltOutlined, DesktopOutlined, DashboardOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { NotificationLayout } from './NotificationLayout';
|
||||
import { NotificationGroup } from './NotificationGroup';
|
||||
import type { NotificationGroupConfig } from './types';
|
||||
|
||||
const GROUPS: NotificationGroupConfig[] = [
|
||||
{
|
||||
icon: <CloudServerOutlined />,
|
||||
title: 'eventGroupOutbound',
|
||||
events: [
|
||||
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
||||
{ key: 'outbound.up', label: 'eventOutboundUp', settingKey: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ThunderboltOutlined />,
|
||||
title: 'eventGroupXray',
|
||||
events: [
|
||||
{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DesktopOutlined />,
|
||||
title: 'eventGroupNode',
|
||||
events: [
|
||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||
{ key: 'node.up', label: 'eventNodeUp', settingKey: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DashboardOutlined />,
|
||||
title: 'eventGroupSystem',
|
||||
events: [
|
||||
{
|
||||
key: 'cpu.high',
|
||||
label: 'eventCPUHigh',
|
||||
settingKey: 'smtpCpu',
|
||||
extra: ({ value, onChange }) => (
|
||||
<InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <SafetyOutlined />,
|
||||
title: 'eventGroupSecurity',
|
||||
events: [
|
||||
{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
allSetting: AllSetting;
|
||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
}
|
||||
|
||||
export function EmailNotifications({ allSetting, updateSetting }: Props) {
|
||||
const events = allSetting.smtpEnabledEvents || '';
|
||||
const selected = events ? events.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||
|
||||
function toggle(key: string) {
|
||||
const next = selected.includes(key)
|
||||
? selected.filter((e) => e !== key)
|
||||
: [...selected, key];
|
||||
updateSetting({ smtpEnabledEvents: next.join(',') });
|
||||
}
|
||||
|
||||
function toggleAll(keys: string[]) {
|
||||
const allSelected = keys.every((v) => selected.includes(v));
|
||||
const next = allSelected
|
||||
? selected.filter((v) => !keys.includes(v))
|
||||
: [...new Set([...selected, ...keys])];
|
||||
updateSetting({ smtpEnabledEvents: next.join(',') });
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationLayout>
|
||||
{GROUPS.map((group, i) => (
|
||||
<NotificationGroup
|
||||
key={i}
|
||||
config={group}
|
||||
selected={selected}
|
||||
onToggle={toggle}
|
||||
onToggleAll={toggleAll}
|
||||
allSetting={allSetting}
|
||||
updateSetting={updateSetting}
|
||||
/>
|
||||
))}
|
||||
</NotificationLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Card } from 'antd';
|
||||
|
||||
interface Props {
|
||||
icon: ReactNode;
|
||||
title: ReactNode;
|
||||
extra: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function NotificationCard({ icon, title, extra, children }: Props) {
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
bordered
|
||||
title={<span>{icon} {title}</span>}
|
||||
extra={extra}
|
||||
style={{ borderWidth: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Checkbox } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onToggle: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function NotificationEvent({ label, checked, onToggle, children }: Props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Checkbox checked={checked} onChange={onToggle}>
|
||||
{t(label)}
|
||||
</Checkbox>
|
||||
{checked && children && (
|
||||
<div style={{ paddingLeft: 24, marginTop: 4 }}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import type { NotificationGroupConfig } from './types';
|
||||
import { NotificationCard } from './NotificationCard';
|
||||
import { NotificationHeader } from './NotificationHeader';
|
||||
import { NotificationEvent } from './NotificationEvent';
|
||||
|
||||
interface Props {
|
||||
config: NotificationGroupConfig;
|
||||
selected: string[];
|
||||
onToggle: (key: string) => void;
|
||||
onToggleAll: (keys: string[]) => void;
|
||||
allSetting: AllSetting;
|
||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
}
|
||||
|
||||
export function NotificationGroup({ config, selected, onToggle, onToggleAll, allSetting, updateSetting }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const count = config.events.filter((e) => selected.includes(e.key)).length;
|
||||
const total = config.events.length;
|
||||
|
||||
function toggleAll() {
|
||||
const values = config.events.map((e) => e.key);
|
||||
onToggleAll(values);
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationCard
|
||||
icon={config.icon}
|
||||
title={t(`pages.settings.${config.title}`)}
|
||||
extra={
|
||||
<NotificationHeader
|
||||
count={count}
|
||||
total={total}
|
||||
allSelected={count === total}
|
||||
indeterminate={count > 0 && count < total}
|
||||
onToggleAll={toggleAll}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
{config.events.map((event) => (
|
||||
<NotificationEvent
|
||||
key={event.key}
|
||||
label={t(`pages.settings.${event.label}`)}
|
||||
checked={selected.includes(event.key)}
|
||||
onToggle={() => onToggle(event.key)}
|
||||
>
|
||||
{event.extra?.({
|
||||
value: Number((allSetting as unknown as Record<string, unknown>)[event.settingKey]) || 0,
|
||||
onChange: (v) => updateSetting({ [event.settingKey]: v }),
|
||||
})}
|
||||
</NotificationEvent>
|
||||
))}
|
||||
</Space>
|
||||
</NotificationCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { Tag } from 'antd';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
total: number;
|
||||
allSelected: boolean;
|
||||
indeterminate: boolean;
|
||||
onToggleAll: () => void;
|
||||
}
|
||||
|
||||
function MasterCheckbox({ checked, indeterminate, onChange }: { checked: boolean; indeterminate: boolean; onChange: () => void }) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.indeterminate = indeterminate;
|
||||
}, [indeterminate]);
|
||||
return <input ref={ref} type="checkbox" checked={checked} onChange={onChange} style={{ cursor: 'pointer' }} />;
|
||||
}
|
||||
|
||||
export function NotificationHeader({ count, total, allSelected, indeterminate, onToggleAll }: Props) {
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag>{count}/{total}</Tag>
|
||||
<MasterCheckbox checked={allSelected} indeterminate={indeterminate} onChange={onToggleAll} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function NotificationLayout({ children }: Props) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 12 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { InputNumber } from 'antd';
|
||||
import { CloudServerOutlined, ThunderboltOutlined, DesktopOutlined, DashboardOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { NotificationLayout } from './NotificationLayout';
|
||||
import { NotificationGroup } from './NotificationGroup';
|
||||
import type { NotificationGroupConfig } from './types';
|
||||
|
||||
const GROUPS: NotificationGroupConfig[] = [
|
||||
{
|
||||
icon: <CloudServerOutlined />,
|
||||
title: 'eventGroupOutbound',
|
||||
events: [
|
||||
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
||||
{ key: 'outbound.up', label: 'eventOutboundUp', settingKey: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ThunderboltOutlined />,
|
||||
title: 'eventGroupXray',
|
||||
events: [
|
||||
{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DesktopOutlined />,
|
||||
title: 'eventGroupNode',
|
||||
events: [
|
||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||
{ key: 'node.up', label: 'eventNodeUp', settingKey: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DashboardOutlined />,
|
||||
title: 'eventGroupSystem',
|
||||
events: [
|
||||
{
|
||||
key: 'cpu.high',
|
||||
label: 'eventCPUHigh',
|
||||
settingKey: 'tgCpu',
|
||||
extra: ({ value, onChange }) => (
|
||||
<InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <SafetyOutlined />,
|
||||
title: 'eventGroupSecurity',
|
||||
events: [
|
||||
{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
allSetting: AllSetting;
|
||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
}
|
||||
|
||||
export function TelegramNotifications({ allSetting, updateSetting }: Props) {
|
||||
const events = allSetting.tgEnabledEvents || '';
|
||||
const selected = events ? events.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||
|
||||
function toggle(key: string) {
|
||||
const next = selected.includes(key)
|
||||
? selected.filter((e) => e !== key)
|
||||
: [...selected, key];
|
||||
updateSetting({ tgEnabledEvents: next.join(',') });
|
||||
}
|
||||
|
||||
function toggleAll(keys: string[]) {
|
||||
const allSelected = keys.every((v) => selected.includes(v));
|
||||
const next = allSelected
|
||||
? selected.filter((v) => !keys.includes(v))
|
||||
: [...new Set([...selected, ...keys])];
|
||||
updateSetting({ tgEnabledEvents: next.join(',') });
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationLayout>
|
||||
{GROUPS.map((group, i) => (
|
||||
<NotificationGroup
|
||||
key={i}
|
||||
config={group}
|
||||
selected={selected}
|
||||
onToggle={toggle}
|
||||
onToggleAll={toggleAll}
|
||||
allSetting={allSetting}
|
||||
updateSetting={updateSetting}
|
||||
/>
|
||||
))}
|
||||
</NotificationLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type { NotificationEventConfig, NotificationGroupConfig } from './types';
|
||||
export { NotificationLayout } from './NotificationLayout';
|
||||
export { NotificationCard } from './NotificationCard';
|
||||
export { NotificationHeader } from './NotificationHeader';
|
||||
export { NotificationEvent } from './NotificationEvent';
|
||||
export { NotificationGroup } from './NotificationGroup';
|
||||
export { TelegramNotifications } from './TelegramNotifications';
|
||||
export { EmailNotifications } from './EmailNotifications';
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface NotificationEventConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
settingKey: string;
|
||||
extra?: (props: { value: number; onChange: (v: number | null) => void }) => ReactNode;
|
||||
}
|
||||
|
||||
export interface NotificationGroupConfig {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
events: NotificationEventConfig[];
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { Alert, Button, Input, InputNumber, Select, Space, Switch, Tabs } from '
|
||||
import { MailOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { SettingListItem, EventBusCheckboxes } from '@/components/ui';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { EmailNotifications } from '@/components/ui/notifications/EmailNotifications';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
|
||||
@@ -122,12 +123,7 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.smtpEventBusNotify')} description={t('pages.settings.smtpEventBusNotifyDesc')}>
|
||||
<EventBusCheckboxes
|
||||
value={allSetting.smtpEnabledEvents}
|
||||
onChange={(v) => updateSetting({ smtpEnabledEvents: v })}
|
||||
extra={{ 'cpu.high': { key: 'smtpCpu', value: allSetting.smtpCpu } }}
|
||||
onExtraChange={(key, v) => updateSetting({ [key]: Number(v) || 0 })}
|
||||
/>
|
||||
<EmailNotifications allSetting={allSetting} updateSetting={updateSetting} />
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -5,7 +5,8 @@ import { BellOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { LanguageManager } from '@/utils';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { SettingListItem, EventBusCheckboxes } from '@/components/ui';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { TelegramNotifications } from '@/components/ui/notifications/TelegramNotifications';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
|
||||
@@ -245,12 +246,7 @@ export default function TelegramTab({ allSetting, updateSetting }: TelegramTabPr
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem paddings="small" title={t('pages.settings.tgEventBusNotify')} description={t('pages.settings.tgEventBusNotifyDesc')}>
|
||||
<EventBusCheckboxes
|
||||
value={allSetting.tgEnabledEvents}
|
||||
onChange={(v) => updateSetting({ tgEnabledEvents: v })}
|
||||
extra={{ 'cpu.high': { key: 'tgCpu', value: allSetting.tgCpu } }}
|
||||
onExtraChange={(key, v) => updateSetting({ [key]: Number(v) || 0 })}
|
||||
/>
|
||||
<TelegramNotifications allSetting={allSetting} updateSetting={updateSetting} />
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user