Files
planet/frontend/src/pages/Alerts/Alerts.tsx
2026-03-05 11:46:58 +08:00

220 lines
6.9 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Table, Tag, Card, Row, Col, Statistic, Button, Modal, Space, Descriptions } from 'antd'
import { AlertOutlined, InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
interface Alert {
id: number
severity: 'critical' | 'warning' | 'info'
status: 'active' | 'acknowledged' | 'resolved'
datasource_name: string
message: string
created_at: string
acknowledged_at?: string
resolved_at?: string
}
function Alerts() {
const { token } = useAuthStore()
const [alerts, setAlerts] = useState<Alert[]>([])
const [loading, setLoading] = useState(false)
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null)
const [detailVisible, setDetailVisible] = useState(false)
const fetchAlerts = async () => {
setLoading(true)
try {
const res = await fetch('/api/v1/alerts', {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
setAlerts(data.data || [])
} catch (error) {
console.error('Failed to fetch alerts:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchAlerts()
}, [token])
const handleAcknowledge = async (alertId: number) => {
try {
await fetch(`/api/v1/alerts/${alertId}/acknowledge`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
fetchAlerts()
} catch (error) {
console.error('Failed to acknowledge alert:', error)
}
}
const handleResolve = async (alertId: number) => {
try {
await fetch(`/api/v1/alerts/${alertId}/resolve`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ resolution: '已处理' }),
})
fetchAlerts()
} catch (error) {
console.error('Failed to resolve alert:', error)
}
}
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{
title: '级别',
dataIndex: 'severity',
key: 'severity',
render: (s: string) => {
const colors: Record<string, string> = { critical: 'error', warning: 'warning', info: 'blue' }
const icons: Record<string, JSX.Element> = {
critical: <AlertOutlined />,
warning: <AlertOutlined />,
info: <InfoCircleOutlined />,
}
return (
<Tag color={colors[s]} icon={icons[s]}>
{s === 'critical' ? '严重' : s === 'warning' ? '警告' : '信息'}
</Tag>
)
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (s: string) => {
const colors: Record<string, string> = { active: 'red', acknowledged: 'orange', resolved: 'green' }
return (
<Tag color={colors[s]}>
{s === 'active' ? '待处理' : s === 'acknowledged' ? '已确认' : '已解决'}
</Tag>
)
},
},
{ title: '数据源', dataIndex: 'datasource_name', key: 'datasource_name' },
{ title: '消息', dataIndex: 'message', key: 'message', ellipsis: true },
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
render: (t: string) => new Date(t).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: Alert) => (
<Space>
{record.status === 'active' && (
<Button type="link" size="small" onClick={() => handleAcknowledge(record.id)}>
</Button>
)}
{record.status !== 'resolved' && (
<Button type="link" size="small" onClick={() => handleResolve(record.id)}>
</Button>
)}
<Button type="link" size="small" onClick={() => { setSelectedAlert(record); setDetailVisible(true); }}>
</Button>
</Space>
),
},
]
const stats = alerts.reduce(
(acc, alert) => {
if (alert.status === 'active') {
acc[alert.severity]++
}
return acc
},
{ critical: 0, warning: 0, info: 0 } as Record<string, number>
)
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={8}>
<Card>
<Statistic
title="严重告警"
value={stats.critical}
valueStyle={{ color: '#ff4d4f' }}
prefix={<AlertOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="警告"
value={stats.warning}
valueStyle={{ color: '#faad14' }}
prefix={<AlertOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="信息" value={stats.info} valueStyle={{ color: '#1890ff' }} prefix={<InfoCircleOutlined />} />
</Card>
</Col>
</Row>
<Card
title="告警列表"
extra={<Button icon={<ReloadOutlined />} onClick={fetchAlerts}></Button>}
>
<Table columns={columns} dataSource={alerts} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} />
</Card>
<Modal
title="告警详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={null}
width={600}
>
{selectedAlert && (
<Descriptions column={1} bordered>
<Descriptions.Item label="ID">{selectedAlert.id}</Descriptions.Item>
<Descriptions.Item label="级别">
<Tag color={selectedAlert.severity === 'critical' ? 'error' : selectedAlert.severity === 'warning' ? 'warning' : 'blue'}>
{selectedAlert.severity}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={selectedAlert.status === 'active' ? 'red' : selectedAlert.status === 'acknowledged' ? 'orange' : 'green'}>
{selectedAlert.status}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="数据源">{selectedAlert.datasource_name}</Descriptions.Item>
<Descriptions.Item label="消息">{selectedAlert.message}</Descriptions.Item>
<Descriptions.Item label="创建时间">{new Date(selectedAlert.created_at).toLocaleString('zh-CN')}</Descriptions.Item>
{selectedAlert.acknowledged_at && (
<Descriptions.Item label="确认时间">
{new Date(selectedAlert.acknowledged_at).toLocaleString('zh-CN')}
</Descriptions.Item>
)}
{selectedAlert.resolved_at && (
<Descriptions.Item label="解决时间">
{new Date(selectedAlert.resolved_at).toLocaleString('zh-CN')}
</Descriptions.Item>
)}
</Descriptions>
)}
</Modal>
</div>
)
}
export default Alerts