381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||
import {
|
||
Button,
|
||
Card,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
message,
|
||
Select,
|
||
Space,
|
||
Switch,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Typography,
|
||
} from 'antd'
|
||
import axios from 'axios'
|
||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||
|
||
const { Title, Text } = Typography
|
||
|
||
interface SystemSettings {
|
||
system_name: string
|
||
refresh_interval: number
|
||
auto_refresh: boolean
|
||
data_retention_days: number
|
||
max_concurrent_tasks: number
|
||
}
|
||
|
||
interface NotificationSettings {
|
||
email_enabled: boolean
|
||
email_address: string
|
||
critical_alerts: boolean
|
||
warning_alerts: boolean
|
||
daily_summary: boolean
|
||
}
|
||
|
||
interface SecuritySettings {
|
||
session_timeout: number
|
||
max_login_attempts: number
|
||
password_policy: string
|
||
}
|
||
|
||
interface CollectorSettings {
|
||
id: number
|
||
name: string
|
||
source: string
|
||
module: string
|
||
priority: string
|
||
frequency_minutes: number
|
||
frequency: string
|
||
is_active: boolean
|
||
last_run_at: string | null
|
||
last_status: string | null
|
||
next_run_at: string | null
|
||
}
|
||
|
||
function SettingsPanel({
|
||
loading,
|
||
children,
|
||
}: {
|
||
loading: boolean
|
||
children: ReactNode
|
||
}) {
|
||
return (
|
||
<div className="settings-pane">
|
||
<Card className="settings-panel-card" loading={loading}>
|
||
<div className="settings-panel-scroll">{children}</div>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Settings() {
|
||
const [loading, setLoading] = useState(true)
|
||
const [savingCollectorId, setSavingCollectorId] = useState<number | null>(null)
|
||
const [collectors, setCollectors] = useState<CollectorSettings[]>([])
|
||
const collectorTableRegionRef = useRef<HTMLDivElement | null>(null)
|
||
const [collectorTableHeight, setCollectorTableHeight] = useState(360)
|
||
const [systemForm] = Form.useForm<SystemSettings>()
|
||
const [notificationForm] = Form.useForm<NotificationSettings>()
|
||
const [securityForm] = Form.useForm<SecuritySettings>()
|
||
|
||
const fetchSettings = async () => {
|
||
try {
|
||
setLoading(true)
|
||
const response = await axios.get('/api/v1/settings')
|
||
systemForm.setFieldsValue(response.data.system)
|
||
notificationForm.setFieldsValue(response.data.notifications)
|
||
securityForm.setFieldsValue(response.data.security)
|
||
setCollectors(response.data.collectors || [])
|
||
} catch (error) {
|
||
message.error('获取系统配置失败')
|
||
console.error(error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchSettings()
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const updateTableHeight = () => {
|
||
const regionHeight = collectorTableRegionRef.current?.offsetHeight || 0
|
||
setCollectorTableHeight(Math.max(220, regionHeight - 56))
|
||
}
|
||
|
||
updateTableHeight()
|
||
|
||
if (typeof ResizeObserver === 'undefined') {
|
||
return undefined
|
||
}
|
||
|
||
const observer = new ResizeObserver(updateTableHeight)
|
||
if (collectorTableRegionRef.current) observer.observe(collectorTableRegionRef.current)
|
||
|
||
return () => observer.disconnect()
|
||
}, [collectors.length])
|
||
|
||
const saveSection = async (section: 'system' | 'notifications' | 'security', values: object) => {
|
||
try {
|
||
await axios.put(`/api/v1/settings/${section}`, values)
|
||
message.success('配置已保存')
|
||
await fetchSettings()
|
||
} catch (error) {
|
||
message.error('保存失败')
|
||
console.error(error)
|
||
}
|
||
}
|
||
|
||
const updateCollectorField = (id: number, field: keyof CollectorSettings, value: string | number | boolean) => {
|
||
setCollectors((prev) =>
|
||
prev.map((collector) => (collector.id === id ? { ...collector, [field]: value } : collector))
|
||
)
|
||
}
|
||
|
||
const saveCollector = async (collector: CollectorSettings) => {
|
||
try {
|
||
setSavingCollectorId(collector.id)
|
||
await axios.put(`/api/v1/settings/collectors/${collector.id}`, {
|
||
is_active: collector.is_active,
|
||
priority: collector.priority,
|
||
frequency_minutes: collector.frequency_minutes,
|
||
})
|
||
message.success(`${collector.name} 配置已更新`)
|
||
await fetchSettings()
|
||
} catch (error) {
|
||
message.error('采集调度配置保存失败')
|
||
console.error(error)
|
||
} finally {
|
||
setSavingCollectorId(null)
|
||
}
|
||
}
|
||
|
||
const collectorColumns = [
|
||
{
|
||
title: '数据源',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
render: (_: string, record: CollectorSettings) => (
|
||
<div>
|
||
<div>{record.name}</div>
|
||
<Text type="secondary">{record.source}</Text>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '层级',
|
||
dataIndex: 'module',
|
||
key: 'module',
|
||
width: 90,
|
||
render: (module: string) => <Tag color="blue">{module}</Tag>,
|
||
},
|
||
{
|
||
title: '优先级',
|
||
dataIndex: 'priority',
|
||
key: 'priority',
|
||
width: 130,
|
||
render: (priority: string, record: CollectorSettings) => (
|
||
<Select
|
||
value={priority}
|
||
style={{ width: '100%' }}
|
||
onChange={(value) => updateCollectorField(record.id, 'priority', value)}
|
||
options={[
|
||
{ value: 'P0', label: 'P0' },
|
||
{ value: 'P1', label: 'P1' },
|
||
{ value: 'P2', label: 'P2' },
|
||
]}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
title: '频率(分钟)',
|
||
dataIndex: 'frequency_minutes',
|
||
key: 'frequency_minutes',
|
||
width: 150,
|
||
render: (value: number, record: CollectorSettings) => (
|
||
<InputNumber
|
||
min={1}
|
||
max={10080}
|
||
value={value}
|
||
style={{ width: '100%' }}
|
||
onChange={(nextValue) => updateCollectorField(record.id, 'frequency_minutes', nextValue || 1)}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
title: '启用',
|
||
dataIndex: 'is_active',
|
||
key: 'is_active',
|
||
width: 90,
|
||
render: (value: boolean, record: CollectorSettings) => (
|
||
<Switch checked={value} onChange={(checked) => updateCollectorField(record.id, 'is_active', checked)} />
|
||
),
|
||
},
|
||
{
|
||
title: '上次执行',
|
||
dataIndex: 'last_run_at',
|
||
key: 'last_run_at',
|
||
width: 180,
|
||
render: (value: string | null) => (value ? new Date(value).toLocaleString('zh-CN') : '-'),
|
||
},
|
||
{
|
||
title: '下次执行',
|
||
dataIndex: 'next_run_at',
|
||
key: 'next_run_at',
|
||
width: 180,
|
||
render: (value: string | null) => (value ? new Date(value).toLocaleString('zh-CN') : '-'),
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'last_status',
|
||
key: 'last_status',
|
||
width: 120,
|
||
render: (value: string | null) => {
|
||
if (!value) return <Tag>未执行</Tag>
|
||
const color = value === 'success' ? 'success' : value === 'failed' ? 'error' : 'default'
|
||
return <Tag color={color}>{value}</Tag>
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 92,
|
||
fixed: 'right' as const,
|
||
render: (_: unknown, record: CollectorSettings) => (
|
||
<Button type="primary" loading={savingCollectorId === record.id} onClick={() => saveCollector(record)}>
|
||
保存
|
||
</Button>
|
||
),
|
||
},
|
||
]
|
||
|
||
const tabItems = [
|
||
{
|
||
key: 'system',
|
||
label: '系统显示',
|
||
children: (
|
||
<SettingsPanel loading={loading}>
|
||
<Form form={systemForm} layout="vertical" onFinish={(values) => saveSection('system', values)}>
|
||
<Form.Item name="system_name" label="系统名称" rules={[{ required: true, message: '请输入系统名称' }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="refresh_interval" label="默认刷新间隔(秒)">
|
||
<InputNumber min={10} max={3600} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="data_retention_days" label="数据保留天数">
|
||
<InputNumber min={1} max={3650} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="max_concurrent_tasks" label="最大并发任务数">
|
||
<InputNumber min={1} max={50} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="auto_refresh" label="自动刷新" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit">保存系统配置</Button>
|
||
</Form>
|
||
</SettingsPanel>
|
||
),
|
||
},
|
||
{
|
||
key: 'notifications',
|
||
label: '通知策略',
|
||
children: (
|
||
<SettingsPanel loading={loading}>
|
||
<Form form={notificationForm} layout="vertical" onFinish={(values) => saveSection('notifications', values)}>
|
||
<Form.Item name="email_enabled" label="启用邮件通知" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item name="email_address" label="通知邮箱">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="critical_alerts" label="严重告警通知" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item name="warning_alerts" label="警告告警通知" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item name="daily_summary" label="每日摘要" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit">保存通知配置</Button>
|
||
</Form>
|
||
</SettingsPanel>
|
||
),
|
||
},
|
||
{
|
||
key: 'security',
|
||
label: '安全策略',
|
||
children: (
|
||
<SettingsPanel loading={loading}>
|
||
<Form form={securityForm} layout="vertical" onFinish={(values) => saveSection('security', values)}>
|
||
<Form.Item name="session_timeout" label="会话超时(分钟)">
|
||
<InputNumber min={5} max={1440} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="max_login_attempts" label="最大登录尝试次数">
|
||
<InputNumber min={1} max={20} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="password_policy" label="密码策略">
|
||
<Select
|
||
options={[
|
||
{ value: 'low', label: '简单' },
|
||
{ value: 'medium', label: '中等' },
|
||
{ value: 'high', label: '严格' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit">保存安全配置</Button>
|
||
</Form>
|
||
</SettingsPanel>
|
||
),
|
||
},
|
||
{
|
||
key: 'collectors',
|
||
label: '采集调度',
|
||
children: (
|
||
<div className="settings-pane">
|
||
<Card
|
||
className="settings-panel-card settings-panel-card--table"
|
||
loading={loading}
|
||
styles={{ body: { padding: 0 } }}
|
||
>
|
||
<div ref={collectorTableRegionRef} className="table-scroll-region data-source-table-region">
|
||
<Table
|
||
rowKey="id"
|
||
columns={collectorColumns}
|
||
dataSource={collectors}
|
||
pagination={false}
|
||
scroll={{ x: 1200, y: collectorTableHeight }}
|
||
tableLayout="fixed"
|
||
size="small"
|
||
/>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<AppLayout>
|
||
<div className="page-shell settings-shell">
|
||
<div className="page-shell__header">
|
||
<div>
|
||
<Title level={3} style={{ marginBottom: 4 }}>系统配置中心</Title>
|
||
<Text type="secondary">这一页现在已经直接连接数据库配置和采集调度,不再只是演示表单。</Text>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="page-shell__body settings-tabs-shell">
|
||
<Tabs className="settings-tabs" items={tabItems} />
|
||
</div>
|
||
</div>
|
||
</AppLayout>
|
||
)
|
||
}
|
||
|
||
export default Settings
|