Files
planet/frontend/src/pages/Alerts/BGPAlerts.tsx
rayd1o 81970a1d05
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.60.0
2026-05-17 02:50:42 +08:00

268 lines
9.3 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { ReloadOutlined, RobotOutlined } from '@ant-design/icons'
import {
Alert,
Button,
Card,
Descriptions,
Modal,
Space,
Spin,
Statistic,
Table,
Tabs,
Tag,
Typography,
message,
type TableColumnsType,
} from 'antd'
import AppLayout from '../../components/AppLayout/AppLayout'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import type { BGPAnomaly, BGPBriefRecord, BGPIncident } from '../../services/situational-awareness'
import { getSituationalAwarenessGateway } from '../../services/situational-awareness'
import { formatDateTimeZhCN } from '../../utils/datetime'
const { Text } = Typography
const gateway = getSituationalAwarenessGateway()
function severityColor(severity: string) {
if (severity === 'critical') return 'red'
if (severity === 'high') return 'orange'
if (severity === 'medium') return 'gold'
return 'blue'
}
export function BGPAlertsPanel() {
const [messageApi, contextHolder] = message.useMessage()
const [loading, setLoading] = useState(false)
const [incidents, setIncidents] = useState<BGPIncident[]>([])
const [anomalies, setAnomalies] = useState<BGPAnomaly[]>([])
const [briefLoading, setBriefLoading] = useState(false)
const [briefModalOpen, setBriefModalOpen] = useState(false)
const [brief, setBrief] = useState<BGPBriefRecord | null>(null)
const loadData = async () => {
setLoading(true)
try {
const [incidentRows, anomalyRows] = await Promise.all([
gateway.getBGPIncidents(50),
gateway.getBGPAnomalies(80),
])
setIncidents(incidentRows)
setAnomalies(anomalyRows)
} catch (error) {
console.error('Failed to load BGP alerts:', error)
messageApi.error('BGP 告警加载失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
void loadData()
}, [])
const summary = useMemo(
() => ({
activeIncidents: incidents.filter((item) => item.status === 'active').length,
criticalIncidents: incidents.filter((item) => item.severity === 'critical').length,
activeAnomalies: anomalies.filter((item) => item.status === 'active').length,
highRiskAnomalies: anomalies.filter((item) => ['critical', 'high'].includes(item.severity)).length,
}),
[anomalies, incidents],
)
const handleGenerateBrief = async () => {
setBriefModalOpen(true)
setBriefLoading(true)
try {
const record = await gateway.generateBGPBrief()
setBrief(record)
messageApi.success('BGP AI 简报已生成')
} catch (error) {
console.error('Failed to generate BGP brief:', error)
messageApi.error('BGP AI 简报生成失败')
} finally {
setBriefLoading(false)
}
}
const incidentColumns: TableColumnsType<BGPIncident> = [
{
title: '开始时间',
dataIndex: 'started_at',
width: 180,
render: (value: string | null) => formatDateTimeZhCN(value),
},
{ title: '类型', dataIndex: 'incident_type', width: 180 },
{
title: '严重度',
dataIndex: 'severity',
width: 120,
render: (value: string) => <Tag color={severityColor(value)}>{value}</Tag>,
},
{
title: '状态',
dataIndex: 'status',
width: 120,
render: (value: string) => <Tag color={value === 'active' ? 'red' : 'blue'}>{value}</Tag>,
},
{
title: '影响前缀',
dataIndex: 'affected_prefixes',
width: 220,
render: (value: string[]) => (value && value.length > 0 ? value.join(', ') : '-'),
},
{ title: '摘要', dataIndex: 'summary', width: 320 },
]
const anomalyColumns: TableColumnsType<BGPAnomaly> = [
{
title: '时间',
dataIndex: 'created_at',
width: 180,
render: (value: string | null) => formatDateTimeZhCN(value),
},
{ title: '类型', dataIndex: 'anomaly_type', width: 180 },
{
title: '严重度',
dataIndex: 'severity',
width: 120,
render: (value: string) => <Tag color={severityColor(value)}>{value}</Tag>,
},
{
title: '状态',
dataIndex: 'status',
width: 120,
render: (value: string) => <Tag color={value === 'active' ? 'red' : 'blue'}>{value}</Tag>,
},
{
title: '前缀',
dataIndex: 'prefix',
width: 200,
render: (value: string | null) => value || '-',
},
{ title: '摘要', dataIndex: 'summary', width: 320 },
]
return (
<AppLayout>
<div className="alerts-tab-panel bgp-alerts-page">
{contextHolder}
<Space className="bgp-alerts-page__stack" direction="vertical" size={16} style={{ width: '100%' }}>
<div className="alerts-tab-panel__head">
<div>
<Text strong>BGP </Text>
<div className="alerts-tab-panel__subtitle"> BGP incidents anomalies </div>
</div>
<Space>
<Button type="primary" icon={<RobotOutlined />} loading={briefLoading} onClick={() => void handleGenerateBrief()}>
BGP AI
</Button>
<Button icon={<ReloadOutlined />} onClick={() => void loadData()}>
</Button>
</Space>
</div>
<Alert type="info" showIcon message="这里聚焦 BGP 风险信号本身,不等同于系统平台运行告警。" />
<Scrollbar className="alerts-summary-scroll bgp-alerts-page__summary-scroll">
<div className="alerts-summary-grid" style={{ gap: '12px' }}>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="活跃事件" value={summary.activeIncidents} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="严重事件" value={summary.criticalIncidents} valueStyle={{ color: '#ff4d4f' }} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="活跃异常" value={summary.activeAnomalies} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="高风险异常" value={summary.highRiskAnomalies} valueStyle={{ color: '#fa8c16' }} /></Card>
</div>
</div>
</Scrollbar>
<Card className="bgp-alerts-page__table-card">
<Tabs
className="bgp-alerts-page__tabs"
items={[
{
key: 'incidents',
label: 'BGP 事件',
children: (
<TableScrollRegion className="bgp-alerts-page__table-region">
<Table<BGPIncident>
columns={incidentColumns}
dataSource={incidents}
loading={loading}
pagination={false}
rowKey="id"
scroll={{ x: 1200, y: 480 }}
tableLayout="fixed"
/>
</TableScrollRegion>
),
},
{
key: 'anomalies',
label: 'BGP 异常',
children: (
<TableScrollRegion className="bgp-alerts-page__table-region">
<Table<BGPAnomaly>
columns={anomalyColumns}
dataSource={anomalies}
loading={loading}
pagination={false}
rowKey="id"
scroll={{ x: 1100, y: 480 }}
tableLayout="fixed"
/>
</TableScrollRegion>
),
},
]}
/>
</Card>
</Space>
<Modal
title="BGP AI 简报"
open={briefModalOpen}
onCancel={() => setBriefModalOpen(false)}
footer={null}
width={920}
className="bgp-page__brief-modal"
style={{ top: 24 }}
styles={{ body: { paddingTop: 12 } }}
>
{briefLoading ? (
<div className="bgp-page__brief-loading">
<Spin tip="正在生成 BGP AI 简报..." />
</div>
) : brief ? (
<Scrollbar className="bgp-page__brief-modal-body">
<Descriptions size="small" column={3} className="bgp-page__brief-meta">
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
<Descriptions.Item label="生成时间">{formatDateTimeZhCN(brief.generated_at)}</Descriptions.Item>
</Descriptions>
<MarkdownRenderer markdown={brief.content_markdown} />
</Scrollbar>
) : (
<Text type="secondary"> BGP </Text>
)}
</Modal>
</div>
</AppLayout>
)
}
export default BGPAlertsPanel