715 lines
21 KiB
TypeScript
715 lines
21 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
||
import { ReloadOutlined } from '@ant-design/icons'
|
||
import {
|
||
Alert,
|
||
Button,
|
||
Card,
|
||
Col,
|
||
Descriptions,
|
||
Row,
|
||
Select,
|
||
Space,
|
||
Spin,
|
||
Statistic,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Typography,
|
||
message,
|
||
type TableColumnsType,
|
||
type TabsProps,
|
||
} from 'antd'
|
||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||
import {
|
||
type BGPAnomaly,
|
||
type BGPBriefRecord,
|
||
type BGPBriefRecordSummary,
|
||
type BGPCollectorCoverage,
|
||
type BGPEvent,
|
||
type BGPIncident,
|
||
type CollectorSummary,
|
||
type EventSummary,
|
||
type Summary,
|
||
getSituationalAwarenessGateway,
|
||
} from '../../services/situational-awareness'
|
||
|
||
const { Title, Text } = Typography
|
||
|
||
const OVERVIEW_OPTIONS = {
|
||
incidentPageSize: 50,
|
||
anomalyPageSize: 100,
|
||
eventPageSize: 20,
|
||
}
|
||
|
||
const DEFAULT_BGP_TAB = 'collectors'
|
||
|
||
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 formatBriefOptions(records: BGPBriefRecordSummary[]) {
|
||
return records.map((item) => ({
|
||
value: item.id,
|
||
label: `${formatDateTimeZhCN(item.generated_at)} · ${item.model}`,
|
||
}))
|
||
}
|
||
|
||
function sortBriefRecords<T extends BGPBriefRecordSummary>(records: T[]) {
|
||
return [...records].sort((left, right) => right.generated_at.localeCompare(left.generated_at))
|
||
}
|
||
|
||
function renderCollectorLocation(_: unknown, record: BGPCollectorCoverage) {
|
||
return [record.city, record.country].filter(Boolean).join(', ') || '-'
|
||
}
|
||
|
||
function renderCollectorLatestEvent(_: unknown, record: BGPCollectorCoverage) {
|
||
const time = formatDateTimeZhCN(record.latest_observed_at)
|
||
return record.latest_event_type ? `${record.latest_event_type} @ ${time}` : time
|
||
}
|
||
|
||
function renderCollectorScope(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 || '-'
|
||
}
|
||
|
||
function renderIncidentCollectors(value: string[]) {
|
||
return value && value.length > 0 ? `${value.length}个 (${value.slice(0, 3).join(', ')})` : '-'
|
||
}
|
||
|
||
function renderIncidentRegions(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(' / ')
|
||
}
|
||
|
||
function renderIncidentCables(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(' / ')
|
||
}
|
||
|
||
function renderPercentage(value: number) {
|
||
return `${Math.round((value || 0) * 100)}%`
|
||
}
|
||
|
||
function renderAnomalyAsn(_: unknown, record: BGPAnomaly) {
|
||
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 '-'
|
||
}
|
||
|
||
function renderOptionalAsn(value: number | null) {
|
||
return value ? `AS${value}` : '-'
|
||
}
|
||
|
||
const collectorColumns: TableColumnsType<BGPCollectorCoverage> = [
|
||
{
|
||
title: '观测站',
|
||
dataIndex: 'collector',
|
||
width: 120,
|
||
},
|
||
{
|
||
title: '位置',
|
||
width: 180,
|
||
render: renderCollectorLocation,
|
||
},
|
||
{
|
||
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: renderCollectorLatestEvent,
|
||
},
|
||
{
|
||
title: '日常覆盖范围',
|
||
dataIndex: 'baseline_scope',
|
||
width: 280,
|
||
render: renderCollectorScope,
|
||
},
|
||
]
|
||
|
||
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: 'affected_prefixes',
|
||
width: 200,
|
||
render: (value: string[]) => (value && value.length > 0 ? value.join(', ') : '-'),
|
||
},
|
||
{
|
||
title: '观测站',
|
||
dataIndex: 'affected_collectors',
|
||
width: 180,
|
||
render: renderIncidentCollectors,
|
||
},
|
||
{
|
||
title: '区域',
|
||
dataIndex: 'affected_regions',
|
||
width: 220,
|
||
render: renderIncidentRegions,
|
||
},
|
||
{
|
||
title: '附近基础设施',
|
||
dataIndex: 'related_cables',
|
||
width: 260,
|
||
render: renderIncidentCables,
|
||
},
|
||
{
|
||
title: '置信度',
|
||
dataIndex: 'confidence',
|
||
width: 120,
|
||
render: renderPercentage,
|
||
},
|
||
{
|
||
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: 'prefix',
|
||
width: 180,
|
||
render: (value: string | null) => value || '-',
|
||
},
|
||
{
|
||
title: 'ASN',
|
||
key: 'asn',
|
||
width: 160,
|
||
render: renderAnomalyAsn,
|
||
},
|
||
{
|
||
title: '来源',
|
||
dataIndex: 'source',
|
||
width: 140,
|
||
},
|
||
{
|
||
title: '置信度',
|
||
dataIndex: 'confidence',
|
||
width: 120,
|
||
render: renderPercentage,
|
||
},
|
||
{
|
||
title: '摘要',
|
||
dataIndex: 'summary',
|
||
width: 320,
|
||
},
|
||
]
|
||
|
||
const eventColumns: TableColumnsType<BGPEvent> = [
|
||
{
|
||
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: renderOptionalAsn,
|
||
},
|
||
{
|
||
title: 'Peer ASN',
|
||
dataIndex: 'peer_asn',
|
||
width: 140,
|
||
render: renderOptionalAsn,
|
||
},
|
||
]
|
||
|
||
async function loadInitialBrief(savedBriefs: BGPBriefRecordSummary[]) {
|
||
const latestBrief = await situationalAwarenessGateway.getLatestBGPBrief()
|
||
if (latestBrief) {
|
||
return latestBrief
|
||
}
|
||
if (savedBriefs[0]) {
|
||
return situationalAwarenessGateway.getBGPBrief(savedBriefs[0].id)
|
||
}
|
||
return null
|
||
}
|
||
|
||
function BGP() {
|
||
const [messageApi, contextHolder] = message.useMessage()
|
||
const [summaryLoading, setSummaryLoading] = useState(false)
|
||
const [activeTab, setActiveTab] = useState(DEFAULT_BGP_TAB)
|
||
const [incidents, setIncidents] = useState<BGPIncident[]>([])
|
||
const [anomalies, setAnomalies] = useState<BGPAnomaly[]>([])
|
||
const [events, setEvents] = useState<BGPEvent[]>([])
|
||
const [collectors, setCollectors] = useState<BGPCollectorCoverage[]>([])
|
||
const [collectorsLoading, setCollectorsLoading] = useState(false)
|
||
const [incidentsLoading, setIncidentsLoading] = useState(false)
|
||
const [anomaliesLoading, setAnomaliesLoading] = useState(false)
|
||
const [eventsLoading, setEventsLoading] = useState(false)
|
||
const [incidentSummary, setIncidentSummary] = useState<Summary | null>(null)
|
||
const [eventSummary, setEventSummary] = useState<EventSummary | null>(null)
|
||
const [collectorSummary, setCollectorSummary] = useState<CollectorSummary | null>(null)
|
||
const [compactViewport, setCompactViewport] = useState(false)
|
||
const [tableHeight, setTableHeight] = useState(360)
|
||
const [briefLoading, setBriefLoading] = useState(false)
|
||
const [briefDetailLoading, setBriefDetailLoading] = useState(false)
|
||
const [brief, setBrief] = useState<BGPBriefRecord | null>(null)
|
||
const [briefOptions, setBriefOptions] = useState<BGPBriefRecordSummary[]>([])
|
||
const [selectedBriefId, setSelectedBriefId] = useState<string | null>(null)
|
||
const [briefListLoaded, setBriefListLoaded] = useState(false)
|
||
const tableRegionRef = useRef<HTMLDivElement | null>(null)
|
||
|
||
useEffect(() => {
|
||
const load = async () => {
|
||
setSummaryLoading(true)
|
||
try {
|
||
const summary = await situationalAwarenessGateway.getBGPSummary()
|
||
|
||
setIncidentSummary(summary.incidentSummary)
|
||
setEventSummary(summary.eventSummary)
|
||
setCollectorSummary(summary.collectorSummary)
|
||
} catch (error) {
|
||
console.error('Failed to load BGP overview:', error)
|
||
} finally {
|
||
setSummaryLoading(false)
|
||
}
|
||
}
|
||
|
||
void load()
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const loadTabData = async () => {
|
||
if (activeTab === 'collectors' && collectors.length === 0 && !collectorsLoading) {
|
||
setCollectorsLoading(true)
|
||
try {
|
||
setCollectors(await situationalAwarenessGateway.getBGPCollectors())
|
||
} catch (error) {
|
||
console.error('Failed to load BGP collectors:', error)
|
||
} finally {
|
||
setCollectorsLoading(false)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (activeTab === 'incidents' && incidents.length === 0 && !incidentsLoading) {
|
||
setIncidentsLoading(true)
|
||
try {
|
||
setIncidents(await situationalAwarenessGateway.getBGPIncidents(OVERVIEW_OPTIONS.incidentPageSize))
|
||
} catch (error) {
|
||
console.error('Failed to load BGP incidents:', error)
|
||
} finally {
|
||
setIncidentsLoading(false)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (activeTab === 'anomalies' && anomalies.length === 0 && !anomaliesLoading) {
|
||
setAnomaliesLoading(true)
|
||
try {
|
||
setAnomalies(await situationalAwarenessGateway.getBGPAnomalies(OVERVIEW_OPTIONS.anomalyPageSize))
|
||
} catch (error) {
|
||
console.error('Failed to load BGP anomalies:', error)
|
||
} finally {
|
||
setAnomaliesLoading(false)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (activeTab === 'events' && events.length === 0 && !eventsLoading) {
|
||
setEventsLoading(true)
|
||
try {
|
||
setEvents(await situationalAwarenessGateway.getBGPEvents(OVERVIEW_OPTIONS.eventPageSize))
|
||
} catch (error) {
|
||
console.error('Failed to load BGP events:', error)
|
||
} finally {
|
||
setEventsLoading(false)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (activeTab === 'brief' && !briefListLoaded && !briefDetailLoading) {
|
||
setBriefDetailLoading(true)
|
||
try {
|
||
const savedBriefs = sortBriefRecords(await situationalAwarenessGateway.listBGPBriefs())
|
||
const initialBrief = await loadInitialBrief(savedBriefs)
|
||
setBriefOptions(savedBriefs)
|
||
setBrief(initialBrief)
|
||
setSelectedBriefId(initialBrief?.id || savedBriefs[0]?.id || null)
|
||
setBriefListLoaded(true)
|
||
} catch (error) {
|
||
console.error('Failed to load BGP briefs:', error)
|
||
} finally {
|
||
setBriefDetailLoading(false)
|
||
}
|
||
}
|
||
}
|
||
|
||
void loadTabData()
|
||
}, [
|
||
activeTab,
|
||
anomalies.length,
|
||
anomaliesLoading,
|
||
briefDetailLoading,
|
||
briefListLoaded,
|
||
collectors.length,
|
||
collectorsLoading,
|
||
events.length,
|
||
eventsLoading,
|
||
incidents.length,
|
||
incidentsLoading,
|
||
])
|
||
|
||
useEffect(() => {
|
||
const updateViewportMode = () => {
|
||
if (typeof window === 'undefined') return
|
||
setCompactViewport(window.innerHeight <= 860 || window.innerWidth <= 1440)
|
||
}
|
||
|
||
updateViewportMode()
|
||
window.addEventListener('resize', updateViewportMode)
|
||
return () => window.removeEventListener('resize', updateViewportMode)
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const updateTableHeight = () => {
|
||
const regionHeight = tableRegionRef.current?.offsetHeight || 0
|
||
const minimumHeight = compactViewport ? 180 : 240
|
||
const verticalOffset = compactViewport ? 40 : 52
|
||
setTableHeight(Math.max(minimumHeight, regionHeight - verticalOffset))
|
||
}
|
||
|
||
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 },
|
||
]
|
||
|
||
const handleGenerateBrief = async () => {
|
||
setBriefLoading(true)
|
||
try {
|
||
const record = await situationalAwarenessGateway.generateBGPBrief()
|
||
setBrief(record)
|
||
setSelectedBriefId(record.id)
|
||
setBriefOptions((current) => sortBriefRecords([record, ...current.filter((item) => item.id !== record.id)]))
|
||
setBriefListLoaded(true)
|
||
messageApi.success('BGP AI 简报已生成')
|
||
} catch (error) {
|
||
console.error('Failed to generate BGP brief:', error)
|
||
messageApi.error('BGP AI 简报生成失败,请检查 AI Provider 或稍后再试')
|
||
} finally {
|
||
setBriefLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleBriefSelectionChange = async (briefId: string) => {
|
||
setSelectedBriefId(briefId)
|
||
setBriefDetailLoading(true)
|
||
try {
|
||
setBrief(await situationalAwarenessGateway.getBGPBrief(briefId))
|
||
} catch (error) {
|
||
console.error('Failed to load saved BGP brief:', error)
|
||
messageApi.error('BGP AI 简报加载失败,请稍后再试')
|
||
} finally {
|
||
setBriefDetailLoading(false)
|
||
}
|
||
}
|
||
|
||
const briefTabContent = (
|
||
<div className="bgp-page__brief-tabpane">
|
||
<div className="bgp-page__brief-card">
|
||
<div className="bgp-page__brief-head">
|
||
<div>
|
||
<Text strong>BGP AI 简报</Text>
|
||
<div className="bgp-page__brief-subtitle">
|
||
每次生成都会保存为 Markdown。打开页面时默认展示最近一次简报,也可以从历史列表切换查看。
|
||
</div>
|
||
</div>
|
||
<div className="bgp-page__brief-actions">
|
||
<Select
|
||
className="bgp-page__brief-select"
|
||
placeholder="选择历史简报"
|
||
value={selectedBriefId || undefined}
|
||
options={formatBriefOptions(briefOptions)}
|
||
onChange={(value) => void handleBriefSelectionChange(value)}
|
||
disabled={briefLoading || briefDetailLoading || briefOptions.length === 0}
|
||
/>
|
||
<Button
|
||
type="primary"
|
||
icon={<ReloadOutlined />}
|
||
loading={briefLoading}
|
||
onClick={() => void handleGenerateBrief()}
|
||
>
|
||
生成 AI 简报
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{briefLoading || briefDetailLoading ? (
|
||
<div className="bgp-page__brief-loading">
|
||
<Spin />
|
||
<Text type="secondary">
|
||
{briefLoading ? '正在整理 BGP 事实并生成简报...' : '正在加载已保存的 BGP 简报...'}
|
||
</Text>
|
||
</div>
|
||
) : brief ? (
|
||
<div className="bgp-page__brief-body">
|
||
<Descriptions size="small" column={compactViewport ? 1 : 3} className="bgp-page__brief-meta">
|
||
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="请求ID">{brief.request_id || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="生成时间" span={compactViewport ? 1 : 3}>
|
||
{formatDateTimeZhCN(brief.generated_at)}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
<div className="bgp-page__brief-content">
|
||
<MarkdownRenderer markdown={brief.content_markdown} />
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="bgp-page__brief-empty">
|
||
<Text type="secondary">还没有生成简报。点击右侧按钮,用当前 BGP 真实数据生成并保存第一份 Markdown 简报。</Text>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
const tabItems: TabsProps['items'] = [
|
||
{
|
||
key: 'collectors',
|
||
label: '观测站覆盖',
|
||
children: (
|
||
<Table<BGPCollectorCoverage>
|
||
rowKey="collector"
|
||
loading={summaryLoading || collectorsLoading}
|
||
dataSource={collectors}
|
||
pagination={false}
|
||
scroll={{ x: 1240, y: tableHeight }}
|
||
tableLayout="fixed"
|
||
columns={collectorColumns}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: 'incidents',
|
||
label: '事件列表',
|
||
children: (
|
||
<Table<BGPIncident>
|
||
rowKey="id"
|
||
loading={summaryLoading || incidentsLoading}
|
||
dataSource={incidents}
|
||
pagination={false}
|
||
scroll={{ x: 1660, y: tableHeight }}
|
||
tableLayout="fixed"
|
||
columns={incidentColumns}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: 'anomalies',
|
||
label: '异常明细',
|
||
children: (
|
||
<Table<BGPAnomaly>
|
||
rowKey="id"
|
||
loading={summaryLoading || anomaliesLoading}
|
||
dataSource={anomalies}
|
||
pagination={false}
|
||
scroll={{ x: 1380, y: tableHeight }}
|
||
tableLayout="fixed"
|
||
columns={anomalyColumns}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: 'events',
|
||
label: '最近观测事件',
|
||
children: (
|
||
<Table<BGPEvent>
|
||
rowKey="id"
|
||
loading={summaryLoading || eventsLoading}
|
||
dataSource={events}
|
||
pagination={false}
|
||
scroll={{ x: 980, y: tableHeight }}
|
||
tableLayout="fixed"
|
||
columns={eventColumns}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: 'brief',
|
||
label: 'AI 简报',
|
||
children: briefTabContent,
|
||
},
|
||
]
|
||
|
||
return (
|
||
<AppLayout>
|
||
{contextHolder}
|
||
<div className={`page-shell bgp-page${compactViewport ? ' bgp-page--compact' : ''}`}>
|
||
<div className="page-shell__header bgp-page__header">
|
||
<div>
|
||
<Title level={3} style={{ marginBottom: 4 }}>BGP观测</Title>
|
||
<Text type="secondary">先看事件态势,再下钻到原子异常明细。</Text>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="page-shell__body bgp-page__body">
|
||
<Space
|
||
className="bgp-page__stack"
|
||
direction="vertical"
|
||
size={compactViewport ? 12 : 16}
|
||
style={{ width: '100%' }}
|
||
>
|
||
<Alert
|
||
className="bgp-page__alert"
|
||
type="info"
|
||
showIcon
|
||
message="该视图展示的是控制平面异常,不代表真实业务流量路径。"
|
||
/>
|
||
|
||
<Card className="bgp-page__summary-card">
|
||
<Row
|
||
gutter={[compactViewport ? 8 : 12, compactViewport ? 8 : 12]}
|
||
className={`bgp-page__summary-grid${compactViewport ? ' bgp-page__summary-grid--compact' : ''}`}
|
||
wrap={!compactViewport}
|
||
>
|
||
{summaryItems.map((item) => (
|
||
<Col
|
||
key={item.label}
|
||
xs={24}
|
||
sm={12}
|
||
md={8}
|
||
flex={compactViewport ? '180px' : undefined}
|
||
>
|
||
<div className="bgp-page__summary-item">
|
||
<div className="bgp-page__summary-label">{item.label}</div>
|
||
<Statistic className="bgp-page__summary-stat" value={item.value} />
|
||
</div>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
</Card>
|
||
|
||
<Card className="bgp-page__table-card">
|
||
<div ref={tableRegionRef} className="table-scroll-region bgp-page__table-region">
|
||
<Tabs
|
||
className="bgp-page__tabs"
|
||
activeKey={activeTab}
|
||
onChange={setActiveTab}
|
||
items={tabItems}
|
||
/>
|
||
</div>
|
||
</Card>
|
||
</Space>
|
||
</div>
|
||
</div>
|
||
</AppLayout>
|
||
)
|
||
}
|
||
|
||
export default BGP
|