mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-28 21:11:23 +08:00
first commit
This commit is contained in:
184
web/src/components/notifications/NotificationFormDrawer.tsx
Normal file
184
web/src/components/notifications/NotificationFormDrawer.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Alert, Button, Drawer, Input, InputNumber, Select, Space, Switch, Typography } from '@arco-design/web-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { NotificationDetail, NotificationPayload, NotificationType } from '../../types/notifications'
|
||||
import { getNotificationFieldConfigs, getNotificationTypeLabel, notificationTypeOptions } from './field-config'
|
||||
|
||||
interface NotificationFormDrawerProps {
|
||||
visible: boolean
|
||||
loading: boolean
|
||||
testing: boolean
|
||||
initialValue: NotificationDetail | null
|
||||
onCancel: () => void
|
||||
onSubmit: (value: NotificationPayload, notificationId?: number) => Promise<void>
|
||||
onTest: (value: NotificationPayload, notificationId?: number) => Promise<void>
|
||||
}
|
||||
|
||||
function createEmptyDraft(): NotificationPayload {
|
||||
return {
|
||||
name: '',
|
||||
type: 'webhook',
|
||||
enabled: true,
|
||||
onSuccess: false,
|
||||
onFailure: true,
|
||||
config: {},
|
||||
}
|
||||
}
|
||||
|
||||
export function NotificationFormDrawer({ visible, loading, testing, initialValue, onCancel, onSubmit, onTest }: NotificationFormDrawerProps) {
|
||||
const [draft, setDraft] = useState<NotificationPayload>(createEmptyDraft())
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
if (!initialValue) {
|
||||
setDraft(createEmptyDraft())
|
||||
setError('')
|
||||
return
|
||||
}
|
||||
setDraft({
|
||||
name: initialValue.name,
|
||||
type: initialValue.type,
|
||||
enabled: initialValue.enabled,
|
||||
onSuccess: initialValue.onSuccess,
|
||||
onFailure: initialValue.onFailure,
|
||||
config: { ...initialValue.config },
|
||||
})
|
||||
setError('')
|
||||
}, [initialValue, visible])
|
||||
|
||||
const fieldConfigs = useMemo(() => getNotificationFieldConfigs(draft.type), [draft.type])
|
||||
|
||||
function updateDraft(patch: Partial<NotificationPayload>) {
|
||||
setDraft((current) => ({ ...current, ...patch }))
|
||||
}
|
||||
|
||||
function updateConfig(key: string, value: string | number) {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
config: {
|
||||
...current.config,
|
||||
[key]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function validate(value: NotificationPayload) {
|
||||
if (!value.name.trim()) {
|
||||
return '请输入通知名称'
|
||||
}
|
||||
for (const field of fieldConfigs) {
|
||||
if (!field.required) {
|
||||
continue
|
||||
}
|
||||
const currentValue = value.config[field.key]
|
||||
if (typeof currentValue === 'number' && currentValue > 0) {
|
||||
continue
|
||||
}
|
||||
if (typeof currentValue === 'string' && currentValue.trim()) {
|
||||
continue
|
||||
}
|
||||
if (initialValue?.maskedFields?.includes(field.key) && (currentValue === '' || currentValue === undefined)) {
|
||||
continue
|
||||
}
|
||||
return `请填写${field.label}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const validationError = validate(draft)
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
await onSubmit(draft, initialValue?.id)
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
const validationError = validate(draft)
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
await onTest(draft, initialValue?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer width={560} title={initialValue ? '编辑通知配置' : '新建通知配置'} visible={visible} onCancel={onCancel} unmountOnExit={false}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{error ? <Alert type="error" content={error} /> : null}
|
||||
<div>
|
||||
<Typography.Text>名称</Typography.Text>
|
||||
<Input value={draft.name} placeholder="例如:生产故障通知" onChange={(value) => updateDraft({ name: value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text>类型</Typography.Text>
|
||||
<Select value={draft.type} options={notificationTypeOptions as unknown as { label: string; value: string }[]} onChange={(value) => updateDraft({ type: value as NotificationType, config: {} })} />
|
||||
</div>
|
||||
<Space align="center" size="medium">
|
||||
<Typography.Text>启用</Typography.Text>
|
||||
<Switch checked={draft.enabled} onChange={(checked) => updateDraft({ enabled: checked })} />
|
||||
</Space>
|
||||
<Space align="center" size="medium">
|
||||
<Typography.Text>成功时通知</Typography.Text>
|
||||
<Switch checked={draft.onSuccess} onChange={(checked) => updateDraft({ onSuccess: checked })} />
|
||||
</Space>
|
||||
<Space align="center" size="medium">
|
||||
<Typography.Text>失败时通知</Typography.Text>
|
||||
<Switch checked={draft.onFailure} onChange={(checked) => updateDraft({ onFailure: checked })} />
|
||||
</Space>
|
||||
<div>
|
||||
<Typography.Title heading={6} style={{ marginTop: 0 }}>
|
||||
{getNotificationTypeLabel(draft.type)} 配置
|
||||
</Typography.Title>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{fieldConfigs.map((field) => {
|
||||
const currentValue = draft.config[field.key]
|
||||
const normalizedValue = typeof currentValue === 'number' || typeof currentValue === 'string' ? currentValue : field.type === 'number' ? 0 : ''
|
||||
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<Typography.Text>
|
||||
{field.label}
|
||||
{field.required ? ' *' : ''}
|
||||
</Typography.Text>
|
||||
{field.type === 'password' ? (
|
||||
<Input.Password value={String(normalizedValue)} placeholder={field.placeholder} onChange={(value) => updateConfig(field.key, value)} />
|
||||
) : field.type === 'number' ? (
|
||||
<InputNumber style={{ width: '100%' }} value={Number(normalizedValue)} min={0} onChange={(value) => updateConfig(field.key, Number(value ?? 0))} />
|
||||
) : field.type === 'textarea' ? (
|
||||
<Input.TextArea value={String(normalizedValue)} placeholder={field.placeholder} onChange={(value) => updateConfig(field.key, value)} />
|
||||
) : (
|
||||
<Input value={String(normalizedValue)} placeholder={field.placeholder} onChange={(value) => updateConfig(field.key, value)} />
|
||||
)}
|
||||
{field.description ? (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
|
||||
{field.description}
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
{initialValue?.maskedFields?.includes(field.key) && !draft.config[field.key] ? (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
|
||||
已存在敏感配置,留空则保持不变。
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
<Space>
|
||||
<Button loading={testing} onClick={handleTest}>
|
||||
发送测试通知
|
||||
</Button>
|
||||
<Button type="primary" loading={loading} onClick={handleSubmit}>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
19
web/src/components/notifications/field-config.test.ts
Normal file
19
web/src/components/notifications/field-config.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getNotificationFieldConfigs, getNotificationTypeLabel } from './field-config'
|
||||
|
||||
describe('notification field config', () => {
|
||||
it('returns readable type labels', () => {
|
||||
expect(getNotificationTypeLabel('email')).toBe('Email')
|
||||
expect(getNotificationTypeLabel('telegram')).toBe('Telegram')
|
||||
})
|
||||
|
||||
it('returns required fields for each notification type', () => {
|
||||
const emailFields = getNotificationFieldConfigs('email')
|
||||
const webhookFields = getNotificationFieldConfigs('webhook')
|
||||
const telegramFields = getNotificationFieldConfigs('telegram')
|
||||
|
||||
expect(emailFields.some((field) => field.key === 'host' && field.required)).toBe(true)
|
||||
expect(webhookFields.some((field) => field.key === 'url' && field.required)).toBe(true)
|
||||
expect(telegramFields.some((field) => field.key === 'botToken' && field.required)).toBe(true)
|
||||
})
|
||||
})
|
||||
43
web/src/components/notifications/field-config.ts
Normal file
43
web/src/components/notifications/field-config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { NotificationFieldConfig, NotificationType } from '../../types/notifications'
|
||||
|
||||
const FIELD_CONFIG_MAP: Record<NotificationType, NotificationFieldConfig[]> = {
|
||||
email: [
|
||||
{ key: 'host', label: 'SMTP Host', type: 'input', required: true, placeholder: 'smtp.example.com' },
|
||||
{ key: 'port', label: 'SMTP Port', type: 'number', required: true, placeholder: '587' },
|
||||
{ key: 'username', label: '用户名', type: 'input', placeholder: '可选' },
|
||||
{ key: 'password', label: '密码', type: 'password', placeholder: '留空表示保持原密码', sensitive: true },
|
||||
{ key: 'from', label: '发件人', type: 'input', required: true, placeholder: 'backupx@example.com' },
|
||||
{ key: 'to', label: '收件人', type: 'input', required: true, placeholder: 'ops@example.com,dev@example.com' },
|
||||
],
|
||||
webhook: [
|
||||
{ key: 'url', label: 'Webhook URL', type: 'input', required: true, placeholder: 'https://hooks.example.com/backupx' },
|
||||
{ key: 'secret', label: '共享密钥', type: 'password', placeholder: '可选', sensitive: true },
|
||||
],
|
||||
telegram: [
|
||||
{ key: 'botToken', label: 'Bot Token', type: 'password', required: true, placeholder: '123456:ABC', sensitive: true },
|
||||
{ key: 'chatId', label: 'Chat ID', type: 'input', required: true, placeholder: '-100xxxxxxxxxx' },
|
||||
],
|
||||
}
|
||||
|
||||
export const notificationTypeOptions = [
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Webhook', value: 'webhook' },
|
||||
{ label: 'Telegram', value: 'telegram' },
|
||||
] as const
|
||||
|
||||
export function getNotificationTypeLabel(type: NotificationType) {
|
||||
switch (type) {
|
||||
case 'email':
|
||||
return 'Email'
|
||||
case 'webhook':
|
||||
return 'Webhook'
|
||||
case 'telegram':
|
||||
return 'Telegram'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
export function getNotificationFieldConfigs(type: NotificationType) {
|
||||
return FIELD_CONFIG_MAP[type]
|
||||
}
|
||||
Reference in New Issue
Block a user