import { useEffect, useRef, useState } from 'react' import { Alert, Card, Col, Row, Space, Statistic, Table, Tabs, Tag, Typography } from 'antd' import AppLayout from '../../components/AppLayout/AppLayout' import { formatDateTimeZhCN } from '../../utils/datetime' import { getSituationalAwarenessGateway, type BGPAnomaly, type BGPCollectorCoverage, type BGPEvent, type BGPIncident, type CollectorSummary, type EventSummary, type Summary, } from '../../services/situational-awareness' const { Title, Text } = Typography const situationalAwarenessGateway = getSituationalAwarenessGateway() function severityColor(severity: string) { if (severity === 'critical') return 'red' if (severity === 'high') return 'orange' if (severity === 'medium') return 'gold' return 'blue' } function BGP() { const [loading, setLoading] = useState(false) const [incidents, setIncidents] = useState([]) const [anomalies, setAnomalies] = useState([]) const [events, setEvents] = useState([]) const [collectors, setCollectors] = useState([]) const [incidentSummary, setIncidentSummary] = useState(null) const [eventSummary, setEventSummary] = useState(null) const [collectorSummary, setCollectorSummary] = useState(null) const [compactViewport, setCompactViewport] = useState(false) const tableRegionRef = useRef(null) const [tableHeight, setTableHeight] = useState(360) useEffect(() => { const load = async () => { setLoading(true) try { const snapshot = await situationalAwarenessGateway.getBGPOverview({ incidentPageSize: 50, anomalyPageSize: 100, eventPageSize: 20, }) setIncidents(snapshot.incidents) setIncidentSummary(snapshot.incidentSummary) setAnomalies(snapshot.anomalies) setEvents(snapshot.events) setEventSummary(snapshot.eventSummary) setCollectors(snapshot.collectors) setCollectorSummary(snapshot.collectorSummary) } catch (error) { console.error('Failed to load BGP overview:', error) } finally { setLoading(false) } } load() }, []) useEffect(() => { const updateViewportMode = () => { if (typeof window === 'undefined') return const compact = window.devicePixelRatio >= 1.5 || window.innerHeight <= 860 || window.innerWidth <= 1440 setCompactViewport(compact) } updateViewportMode() window.addEventListener('resize', updateViewportMode) return () => window.removeEventListener('resize', updateViewportMode) }, []) useEffect(() => { const updateTableHeight = () => { const regionHeight = tableRegionRef.current?.offsetHeight || 0 setTableHeight(Math.max(compactViewport ? 180 : 240, regionHeight - (compactViewport ? 40 : 52))) } updateTableHeight() if (typeof ResizeObserver === 'undefined') { return undefined } const observer = new ResizeObserver(updateTableHeight) if (tableRegionRef.current) observer.observe(tableRegionRef.current) return () => observer.disconnect() }, [compactViewport, collectors.length, incidents.length, anomalies.length, events.length]) const summaryItems = [ { label: '近24h事件', value: collectorSummary?.recent_24h_events || 0 }, { label: '活跃观测站', value: collectorSummary?.active_collectors || 0 }, { label: '观测前缀', value: collectorSummary?.observed_prefixes || eventSummary?.prefix_count || 0 }, { label: '事件总数', value: incidentSummary?.total || 0 }, { label: '活跃事件', value: incidentSummary?.by_status?.active || 0 }, { label: '严重事件', value: incidentSummary?.by_severity?.critical || 0 }, ] return (
BGP观测 先看事件态势,再下钻到原子异常明细。
{summaryItems.map((item) => (
{item.label}
))}
rowKey="collector" loading={loading} dataSource={collectors} pagination={{ pageSize: compactViewport ? 6 : 8 }} scroll={{ x: 'max-content', y: tableHeight }} tableLayout="fixed" columns={[ { title: '观测站', dataIndex: 'collector', width: 120, }, { title: '位置', width: 180, render: (_, record) => [record.city, record.country].filter(Boolean).join(', ') || '-', }, { title: '近24h事件数', dataIndex: 'recent_24h_observation_count', width: 120, }, { title: '近7d事件数', dataIndex: 'recent_7d_observation_count', width: 120, }, { title: '前缀数', dataIndex: 'prefix_count', width: 120, }, { title: 'Origin ASN 数', dataIndex: 'origin_asn_count', width: 140, }, { title: '最近事件', width: 220, render: (_, record) => { const time = formatDateTimeZhCN(record.latest_observed_at) return record.latest_event_type ? `${record.latest_event_type} @ ${time}` : time }, }, { title: '日常覆盖范围', dataIndex: 'baseline_scope', render: (value: BGPCollectorCoverage['baseline_scope']) => { const cities = value?.cities?.slice(0, 3).join(' / ') || '' const countries = value?.countries?.slice(0, 3).join(' / ') || '' return cities && countries ? `${cities} | ${countries}` : cities || countries || '-' }, }, ]} /> ), }, { key: 'incidents', label: '事件列表', children: ( rowKey="id" loading={loading} dataSource={incidents} pagination={{ pageSize: compactViewport ? 6 : 8 }} scroll={{ x: 'max-content', y: tableHeight }} tableLayout="fixed" columns={[ { 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) => {value}, }, { title: '影响前缀', dataIndex: 'affected_prefixes', width: 200, render: (value: string[]) => (value && value.length > 0 ? value.join(', ') : '-'), }, { title: '观测站', dataIndex: 'affected_collectors', width: 180, render: (value: string[]) => (value && value.length > 0 ? `${value.length}个 (${value.slice(0, 3).join(', ')})` : '-'), }, { title: '区域', dataIndex: 'affected_regions', width: 220, render: (value: Array<{ country?: string; city?: string }>) => { if (!value || value.length === 0) return '-' return value .slice(0, 3) .map((item) => [item.city, item.country].filter(Boolean).join(', ')) .join(' / ') }, }, { title: '附近基础设施', dataIndex: 'related_cables', width: 260, render: (value: BGPIncident['related_cables']) => { if (!value || value.length === 0) return '-' return value .slice(0, 2) .map((item) => { const landing = item.landing_point || [item.city, item.country].filter(Boolean).join(', ') const cable = item.cable_names && item.cable_names.length > 0 ? item.cable_names[0] : '附近登陆点' const distance = item.distance_km !== undefined ? ` ${item.distance_km}km` : '' return `${landing} (${cable}${distance})` }) .join(' / ') }, }, { title: '置信度', dataIndex: 'confidence', width: 120, render: (value: number) => `${Math.round((value || 0) * 100)}%`, }, { title: '摘要', dataIndex: 'summary', }, ]} /> ), }, { key: 'anomalies', label: '异常明细', children: ( rowKey="id" loading={loading} dataSource={anomalies} pagination={{ pageSize: compactViewport ? 8 : 10 }} scroll={{ x: 'max-content', y: tableHeight }} tableLayout="fixed" columns={[ { 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) => {value}, }, { title: '前缀', dataIndex: 'prefix', width: 180, render: (value: string | null) => value || '-', }, { title: 'ASN', key: 'asn', width: 160, render: (_, record) => { if (record.origin_asn && record.new_origin_asn) { return `AS${record.origin_asn} -> AS${record.new_origin_asn}` } if (record.origin_asn) { return `AS${record.origin_asn}` } return '-' }, }, { title: '来源', dataIndex: 'source', width: 140, }, { title: '置信度', dataIndex: 'confidence', width: 120, render: (value: number) => `${Math.round((value || 0) * 100)}%`, }, { title: '摘要', dataIndex: 'summary', }, ]} /> ), }, { key: 'events', label: '最近观测事件', children: ( rowKey="id" loading={loading} dataSource={events} pagination={{ pageSize: compactViewport ? 6 : 8 }} scroll={{ x: 'max-content', y: tableHeight }} tableLayout="fixed" columns={[ { title: '时间', dataIndex: 'observed_at', width: 180, render: (value: string | null) => formatDateTimeZhCN(value), }, { title: '观测站', dataIndex: 'collector', width: 140, render: (value: string | null) => value || '-', }, { title: '类型', dataIndex: 'event_type', width: 120, }, { title: '前缀', dataIndex: 'prefix', width: 200, render: (value: string | null) => value || '-', }, { title: 'Origin ASN', dataIndex: 'origin_asn', width: 140, render: (value: number | null) => (value ? `AS${value}` : '-'), }, { title: 'Peer ASN', dataIndex: 'peer_asn', width: 140, render: (value: number | null) => (value ? `AS${value}` : '-'), }, ]} /> ), }, ]} />
) } export default BGP