Bump version to 0.23.1

This commit is contained in:
rayd1o
2026-04-07 23:51:44 +08:00
parent 7abf391c74
commit 981617ee80
6 changed files with 574 additions and 293 deletions

View File

@@ -1 +1 @@
0.23.0
0.23.1

View File

@@ -7,6 +7,23 @@ This project follows the repository versioning rule:
- `feature` -> `+0.1.0`
- `bugfix` -> `+0.0.1`
## 0.23.1
Released: 2026-04-07
### Highlights
- Fixed the BGP overview page so high-DPI and lower-height screens can keep the observation workspace visible within a single viewport.
### Improved
- Improved [BGP.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/BGP/BGP.tsx) by switching the page to a height-aware shell, compact summary layout, and tabbed data workspace so observation tables retain the majority of the screen.
- Improved [index.css](/home/ray/dev/linkong/planet/frontend/src/index.css) by adding responsive BGP-specific compact spacing, denser table paddings, and an internal scroll region for collector coverage and event tables.
### Fixed
- Fixed the BGP observation page on small or high-scale displays where stacked stats and full-height blocks consumed too much vertical space, leaving only a couple of visible table rows.
## 0.23.0
Released: 2026-04-07

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.23.0",
"version": "0.23.1",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.2.6",

View File

@@ -483,6 +483,175 @@ body {
background: transparent;
}
.bgp-page {
min-height: 0;
}
.bgp-page__header {
align-items: flex-start;
}
.bgp-page__body {
gap: 10px;
}
.bgp-page__body > .ant-space {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.bgp-page__body > .ant-space > .ant-space-item:last-child {
flex: 1 1 auto;
min-height: 0;
display: flex;
}
.bgp-page__alert .ant-alert {
padding: 8px 12px;
}
.bgp-page__alert .ant-alert-message {
font-size: 13px;
}
.bgp-page__summary-card .ant-card-body {
padding: 10px 12px;
}
.bgp-page__summary-item {
min-height: 64px;
padding: 8px 10px;
border-radius: 10px;
background: #f7f8fa;
border: 1px solid #eef1f5;
}
.bgp-page__summary-label {
margin-bottom: 4px;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
line-height: 1.2;
}
.bgp-page__summary-stat.ant-statistic {
line-height: 1;
}
.bgp-page__summary-stat .ant-statistic-content {
font-size: 24px;
line-height: 1.1;
}
.bgp-page__summary-stat .ant-statistic-content-value {
line-height: 1;
}
.bgp-page__table-card,
.bgp-page__table-card .ant-card-body {
min-height: 0;
height: 100%;
}
.bgp-page__table-card .ant-card-body {
display: flex;
flex-direction: column;
padding: 10px 12px;
}
.bgp-page__table-region {
flex: 1 1 auto;
min-height: 0;
height: 100%;
}
.bgp-page__tabs,
.bgp-page__tabs .ant-tabs-content-holder,
.bgp-page__tabs .ant-tabs-content,
.bgp-page__tabs .ant-tabs-tabpane {
min-height: 0;
height: 100%;
}
.bgp-page__tabs {
display: flex;
flex-direction: column;
}
.bgp-page__tabs .ant-tabs-nav {
flex: 0 0 auto;
margin-bottom: 8px;
}
.bgp-page__tabs .ant-tabs-content-holder {
flex: 1 1 auto;
}
.bgp-page__tabs .ant-tabs-tabpane {
display: flex;
flex-direction: column;
}
.bgp-page__tabs .ant-table-wrapper {
flex: 1 1 auto;
min-height: 0;
}
.bgp-page--compact .page-shell__body {
gap: 8px;
}
.bgp-page--compact .bgp-page__alert .ant-alert {
padding: 6px 10px;
}
.bgp-page--compact .bgp-page__alert .ant-alert-message {
font-size: 12px;
}
.bgp-page--compact .bgp-page__summary-card .ant-card-body {
padding: 8px 10px;
}
.bgp-page--compact .bgp-page__summary-item {
min-height: 56px;
padding: 6px 8px;
}
.bgp-page--compact .bgp-page__summary-label {
margin-bottom: 2px;
font-size: 11px;
}
.bgp-page--compact .bgp-page__summary-stat .ant-statistic-content {
font-size: 20px;
}
.bgp-page--compact .bgp-page__table-card .ant-card-body {
padding: 8px 10px;
}
.bgp-page--compact .bgp-page__tabs .ant-tabs-nav {
margin-bottom: 6px;
}
.bgp-page--compact .bgp-page__tabs .ant-tabs-tab {
padding-top: 6px;
padding-bottom: 6px;
}
.bgp-page--compact .bgp-page__tabs .ant-table-thead > tr > th {
font-size: 12px;
line-height: 1.2;
}
.bgp-page--compact .table-scroll-region .ant-table-thead > tr > th,
.bgp-page--compact .table-scroll-region .ant-table-tbody > tr > td {
padding: 6px 8px !important;
}
.data-list-controls-shell {
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
@@ -1056,6 +1225,22 @@ body {
padding: 12px;
}
.bgp-page__body {
gap: 10px;
}
.bgp-page__summary-card .ant-card-body {
padding: 8px 10px;
}
.bgp-page__summary-item {
min-height: 54px;
}
.bgp-page__table-card .ant-card-body {
padding: 8px 10px;
}
.data-list-workspace {
gap: 10px;
}
@@ -1112,6 +1297,29 @@ body {
}
@media (max-width: 640px) {
.bgp-page__header {
align-items: flex-start;
flex-direction: column;
gap: 8px;
}
.bgp-page__stats-row {
margin-bottom: 0 !important;
}
.bgp-page__summary-label {
font-size: 11px;
}
.bgp-page__summary-stat .ant-statistic-content {
font-size: 18px;
}
.bgp-page__tabs .ant-tabs-tab {
padding-left: 8px;
padding-right: 8px;
}
.data-list-summary-treemap {
grid-template-columns: 1fr;
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Alert, Card, Col, Row, Space, Statistic, Table, Tag, Typography } from 'antd'
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 {
@@ -32,6 +32,9 @@ function BGP() {
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 tableRegionRef = useRef<HTMLDivElement | null>(null)
const [tableHeight, setTableHeight] = useState(360)
useEffect(() => {
const load = async () => {
@@ -59,302 +62,355 @@ function BGP() {
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 (
<AppLayout>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<div>
<Title level={3} style={{ marginBottom: 4 }}>BGP观测</Title>
<Text type="secondary"></Text>
<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>
<Alert
type="info"
showIcon
message="该视图展示的是控制平面异常,不代表真实业务流量路径。"
/>
<div className="page-shell__body bgp-page__body">
<Space direction="vertical" size={compactViewport ? 12 : 16} style={{ width: '100%' }}>
<Alert
className="bgp-page__alert"
type="info"
showIcon
message="该视图展示的是控制平面异常,不代表真实业务流量路径。"
/>
<Row gutter={16}>
<Col xs={24} md={8}>
<Card>
<Statistic title="近24h事件" value={collectorSummary?.recent_24h_events || 0} />
<Card className="bgp-page__summary-card">
<Row gutter={[compactViewport ? 8 : 12, compactViewport ? 8 : 12]} className="bgp-page__summary-grid">
{summaryItems.map((item) => (
<Col key={item.label} xs={8} sm={8} md={4}>
<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>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="活跃观测站" value={collectorSummary?.active_collectors || 0} />
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="观测前缀" value={collectorSummary?.observed_prefixes || eventSummary?.prefix_count || 0} />
</Card>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={8}>
<Card>
<Statistic title="事件总数" value={incidentSummary?.total || 0} />
<Card className="bgp-page__table-card">
<div ref={tableRegionRef} className="table-scroll-region bgp-page__table-region">
<Tabs
className="bgp-page__tabs"
items={[
{
key: 'collectors',
label: '观测站覆盖',
children: (
<Table<BGPCollectorCoverage>
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: (
<Table<BGPIncident>
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) => <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: (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: (
<Table<BGPAnomaly>
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) => <Tag color={severityColor(value)}>{value}</Tag>,
},
{
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: (
<Table<BGPEvent>
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}` : '-'),
},
]}
/>
),
},
]}
/>
</div>
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="活跃事件" value={incidentSummary?.by_status?.active || 0} />
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="严重事件" value={incidentSummary?.by_severity?.critical || 0} />
</Card>
</Col>
</Row>
<Card title="观测站覆盖">
<Table<BGPCollectorCoverage>
rowKey="collector"
loading={loading}
dataSource={collectors}
pagination={{ pageSize: 8 }}
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 || '-'
},
},
]}
/>
</Card>
<Card title="事件列表">
<Table<BGPIncident>
rowKey="id"
loading={loading}
dataSource={incidents}
pagination={{ pageSize: 8 }}
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) => <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: (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',
},
]}
/>
</Card>
<Card title="异常明细">
<Table<BGPAnomaly>
rowKey="id"
loading={loading}
dataSource={anomalies}
pagination={{ pageSize: 10 }}
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) => <Tag color={severityColor(value)}>{value}</Tag>,
},
{
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',
},
]}
/>
</Card>
<Card title="最近观测事件">
<Table<BGPEvent>
rowKey="id"
loading={loading}
dataSource={events}
pagination={{ pageSize: 8 }}
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}` : '-'),
},
]}
/>
</Card>
</Space>
</Space>
</div>
</div>
</AppLayout>
)
}

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.23.0"
version = "0.23.1"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [