feat: improve bgp incident visibility
This commit is contained in:
@@ -204,9 +204,17 @@
|
||||
<span class="stats-value" id="satellite-count">0 颗</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label">BGP异常:</span>
|
||||
<span class="stats-label">BGP事件:</span>
|
||||
<span class="stats-value" id="bgp-anomaly-count">0 条</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label">观测站:</span>
|
||||
<span class="stats-value" id="bgp-collector-count">0 个</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label">BGP态势:</span>
|
||||
<span class="stats-value" id="bgp-status-summary">暂无观测数据</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label">视角距离:</span>
|
||||
<span class="stats-value" id="camera-distance">300 km</span>
|
||||
@@ -220,7 +228,7 @@
|
||||
<div id="loading">
|
||||
<div id="loading-spinner"></div>
|
||||
<div id="loading-title">正在初始化全球态势数据...</div>
|
||||
<div id="loading-subtitle" style="font-size:0.9rem; margin-top:10px; color:#aaa;">同步卫星、海底光缆、登陆点与BGP异常数据</div>
|
||||
<div id="loading-subtitle" style="font-size:0.9rem; margin-top:10px; color:#aaa;">同步卫星、海底光缆、登陆点与BGP态势数据</div>
|
||||
</div>
|
||||
<div id="status-message" class="status-message" style="display: none;"></div>
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
|
||||
@@ -7,10 +7,11 @@ const bgpGroup = new THREE.Group();
|
||||
const bgpOverlayGroup = new THREE.Group();
|
||||
const collectorMarkers = [];
|
||||
const anomalyMarkers = [];
|
||||
const anomalyCountByCollector = new Map();
|
||||
const activeEventCountByCollector = new Map();
|
||||
|
||||
let showBGP = true;
|
||||
let totalAnomalyCount = 0;
|
||||
let totalIncidentCount = 0;
|
||||
let textureCache = null;
|
||||
let activeEventOverlay = null;
|
||||
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
|
||||
@@ -328,6 +329,78 @@ function buildAnomalyFeatureData(feature) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildIncidentFeatureData(feature) {
|
||||
const coordinates = feature?.geometry?.coordinates || [];
|
||||
const [longitude, latitude] = coordinates;
|
||||
if (
|
||||
typeof latitude !== "number" ||
|
||||
typeof longitude !== "number" ||
|
||||
Number.isNaN(latitude) ||
|
||||
Number.isNaN(longitude)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const properties = feature?.properties || {};
|
||||
const severity = normalizeSeverity(properties.severity);
|
||||
const startedAt = properties.started_at || properties.created_at || null;
|
||||
const affectedPrefixes = Array.isArray(properties.affected_prefixes)
|
||||
? properties.affected_prefixes
|
||||
: [];
|
||||
const affectedAsns = Array.isArray(properties.affected_asns)
|
||||
? properties.affected_asns
|
||||
: [];
|
||||
const affectedCollectors = Array.isArray(properties.affected_collectors)
|
||||
? properties.affected_collectors
|
||||
: [];
|
||||
const affectedRegions = Array.isArray(properties.affected_regions)
|
||||
? properties.affected_regions
|
||||
: [];
|
||||
|
||||
const primaryRegion = affectedRegions[0] || {};
|
||||
|
||||
return {
|
||||
latitude,
|
||||
longitude,
|
||||
rawSeverity: properties.severity || severity,
|
||||
severity,
|
||||
collector: affectedCollectors[0] || primaryRegion.collector || "-",
|
||||
city: primaryRegion.city || "-",
|
||||
country: primaryRegion.country || "-",
|
||||
source: "bgp_incident",
|
||||
anomaly_type: properties.incident_type || properties.title || "-",
|
||||
incident_type: properties.incident_type || "-",
|
||||
incident_key: properties.incident_key || "-",
|
||||
status: properties.status || "-",
|
||||
prefix: affectedPrefixes[0] || "-",
|
||||
prefixes: affectedPrefixes,
|
||||
origin_asn: affectedAsns[0] ?? "-",
|
||||
new_origin_asn: affectedAsns[1] ?? "-",
|
||||
affected_asns: affectedAsns,
|
||||
as_path: [],
|
||||
collectors: affectedCollectors,
|
||||
collector_count: affectedCollectors.length || 1,
|
||||
impacted_regions: affectedRegions,
|
||||
confidence: properties.confidence ?? "-",
|
||||
summary: properties.summary || properties.title || "-",
|
||||
created_at: formatLocalDateTime(startedAt),
|
||||
created_at_raw: startedAt,
|
||||
route_change:
|
||||
affectedAsns.length > 1
|
||||
? affectedAsns.slice(0, 2).map((asn) => `AS${asn}`).join(" -> ")
|
||||
: affectedPrefixes.length > 1
|
||||
? `${affectedPrefixes.length}个前缀簇`
|
||||
: properties.incident_type || "-",
|
||||
observed_by: formatBGPObservedBy(affectedCollectors),
|
||||
impacted_scope: formatBGPImpactedScope(affectedRegions),
|
||||
location: formatBGPLocation(primaryRegion.city, primaryRegion.country),
|
||||
id:
|
||||
properties.id ||
|
||||
properties.incident_key ||
|
||||
`${properties.incident_type || "incident"}-${latitude}-${longitude}`,
|
||||
};
|
||||
}
|
||||
|
||||
function clearMarkerArray(markers) {
|
||||
while (markers.length > 0) {
|
||||
const marker = markers.pop();
|
||||
@@ -454,9 +527,9 @@ function dedupeAnomalies(features) {
|
||||
const data = buildAnomalyFeatureData(feature);
|
||||
if (!data) return;
|
||||
|
||||
anomalyCountByCollector.set(
|
||||
activeEventCountByCollector.set(
|
||||
data.collector,
|
||||
(anomalyCountByCollector.get(data.collector) || 0) + 1,
|
||||
(activeEventCountByCollector.get(data.collector) || 0) + 1,
|
||||
);
|
||||
|
||||
const dedupeKey = `${data.collector}|${data.latitude.toFixed(4)}|${data.longitude.toFixed(4)}`;
|
||||
@@ -482,39 +555,85 @@ function dedupeAnomalies(features) {
|
||||
.slice(0, BGP_CONFIG.maxRenderedMarkers);
|
||||
}
|
||||
|
||||
function dedupeIncidents(features) {
|
||||
const latestByKey = new Map();
|
||||
|
||||
features.forEach((feature) => {
|
||||
const data = buildIncidentFeatureData(feature);
|
||||
if (!data) return;
|
||||
|
||||
data.collectors.forEach((collector) => {
|
||||
activeEventCountByCollector.set(
|
||||
collector,
|
||||
(activeEventCountByCollector.get(collector) || 0) + 1,
|
||||
);
|
||||
});
|
||||
|
||||
const dedupeKey = String(data.incident_key || data.id);
|
||||
const previous = latestByKey.get(dedupeKey);
|
||||
const currentTime = data.created_at_raw
|
||||
? new Date(data.created_at_raw).getTime()
|
||||
: 0;
|
||||
const previousTime = previous?.created_at_raw
|
||||
? new Date(previous.created_at_raw).getTime()
|
||||
: 0;
|
||||
|
||||
if (!previous || currentTime >= previousTime) {
|
||||
latestByKey.set(dedupeKey, data);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(latestByKey.values())
|
||||
.sort((a, b) => {
|
||||
const timeA = a.created_at_raw ? new Date(a.created_at_raw).getTime() : 0;
|
||||
const timeB = b.created_at_raw ? new Date(b.created_at_raw).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
})
|
||||
.slice(0, BGP_CONFIG.maxRenderedMarkers);
|
||||
}
|
||||
|
||||
function applyCollectorCounts() {
|
||||
collectorMarkers.forEach((marker) => {
|
||||
marker.userData.anomaly_count =
|
||||
anomalyCountByCollector.get(marker.userData.collector) || 0;
|
||||
activeEventCountByCollector.get(marker.userData.collector) || 0;
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadBGPAnomalies(scene, earth) {
|
||||
clearBGPData(earth);
|
||||
|
||||
const [collectorsResponse, anomaliesResponse] = await Promise.all([
|
||||
const [collectorsResponse, incidentsResponse, anomaliesResponse] = await Promise.all([
|
||||
fetch(PATHS.bgpCollectorsApi),
|
||||
fetch(`${PATHS.bgpIncidentsApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
|
||||
fetch(`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
|
||||
]);
|
||||
|
||||
if (!collectorsResponse.ok) {
|
||||
throw new Error(`BGP collectors HTTP ${collectorsResponse.status}`);
|
||||
}
|
||||
if (!incidentsResponse.ok) {
|
||||
throw new Error(`BGP incidents HTTP ${incidentsResponse.status}`);
|
||||
}
|
||||
if (!anomaliesResponse.ok) {
|
||||
throw new Error(`BGP anomalies HTTP ${anomaliesResponse.status}`);
|
||||
}
|
||||
|
||||
const collectorsPayload = await collectorsResponse.json();
|
||||
const incidentsPayload = await incidentsResponse.json();
|
||||
const anomaliesPayload = await anomaliesResponse.json();
|
||||
const collectorFeatures = Array.isArray(collectorsPayload?.features)
|
||||
? collectorsPayload.features
|
||||
: [];
|
||||
const incidentFeatures = Array.isArray(incidentsPayload?.features)
|
||||
? incidentsPayload.features
|
||||
: [];
|
||||
const anomalyFeatures = Array.isArray(anomaliesPayload?.features)
|
||||
? anomaliesPayload.features
|
||||
: [];
|
||||
|
||||
totalAnomalyCount = anomaliesPayload?.count ?? anomalyFeatures.length;
|
||||
anomalyCountByCollector.clear();
|
||||
totalIncidentCount = incidentsPayload?.count ?? incidentFeatures.length;
|
||||
activeEventCountByCollector.clear();
|
||||
|
||||
spreadCollectorPositions(
|
||||
collectorFeatures
|
||||
@@ -522,7 +641,12 @@ export async function loadBGPAnomalies(scene, earth) {
|
||||
.filter(Boolean),
|
||||
).forEach(createCollectorMarker);
|
||||
|
||||
dedupeAnomalies(anomalyFeatures).forEach(createAnomalyMarker);
|
||||
const incidentMarkers = dedupeIncidents(incidentFeatures);
|
||||
if (incidentMarkers.length > 0) {
|
||||
incidentMarkers.forEach(createAnomalyMarker);
|
||||
} else {
|
||||
dedupeAnomalies(anomalyFeatures).forEach(createAnomalyMarker);
|
||||
}
|
||||
applyCollectorCounts();
|
||||
|
||||
if (!bgpGroup.parent) {
|
||||
@@ -540,7 +664,8 @@ export async function loadBGPAnomalies(scene, earth) {
|
||||
}
|
||||
|
||||
return {
|
||||
totalCount: totalAnomalyCount,
|
||||
totalCount: totalIncidentCount,
|
||||
anomalyCount: totalAnomalyCount,
|
||||
renderedCount: anomalyMarkers.length,
|
||||
collectorCount: collectorMarkers.length,
|
||||
};
|
||||
@@ -644,8 +769,9 @@ export function clearBGPData(earth) {
|
||||
clearMarkerArray(collectorMarkers);
|
||||
clearMarkerArray(anomalyMarkers);
|
||||
clearBGPEventOverlay();
|
||||
anomalyCountByCollector.clear();
|
||||
activeEventCountByCollector.clear();
|
||||
totalAnomalyCount = 0;
|
||||
totalIncidentCount = 0;
|
||||
|
||||
if (earth && bgpGroup.parent === earth) {
|
||||
earth.remove(bgpGroup);
|
||||
@@ -684,7 +810,27 @@ export function getBGPCollectorMarkers() {
|
||||
}
|
||||
|
||||
export function getBGPCount() {
|
||||
return totalAnomalyCount;
|
||||
return totalIncidentCount;
|
||||
}
|
||||
|
||||
export function getBGPCollectorCount() {
|
||||
return collectorMarkers.length;
|
||||
}
|
||||
|
||||
export function getBGPStatusSummary() {
|
||||
if (totalIncidentCount > 0 && totalAnomalyCount > 0) {
|
||||
return `${totalIncidentCount} 起活跃事件 / ${totalAnomalyCount} 条异常`;
|
||||
}
|
||||
if (totalIncidentCount > 0) {
|
||||
return `${totalIncidentCount} 起活跃事件`;
|
||||
}
|
||||
if (totalAnomalyCount > 0) {
|
||||
return `${totalAnomalyCount} 条活跃异常`;
|
||||
}
|
||||
if (collectorMarkers.length > 0) {
|
||||
return "当前无活跃事件";
|
||||
}
|
||||
return "暂无观测数据";
|
||||
}
|
||||
|
||||
export function showBGPEventOverlay(marker, earth) {
|
||||
@@ -779,9 +925,9 @@ export function getBGPLegendItems() {
|
||||
{ color: "#6db7ff", label: "观测站" },
|
||||
{ color: "#8af5ff", label: "事件连线 / 枢纽" },
|
||||
{ color: "#2dd4bf", label: "影响区域" },
|
||||
{ color: "#ff4d4f", label: "严重异常" },
|
||||
{ color: "#ff9f43", label: "高危异常" },
|
||||
{ color: "#ffd166", label: "中危异常" },
|
||||
{ color: "#4dabf7", label: "低危异常" },
|
||||
{ color: "#ff4d4f", label: "严重事件" },
|
||||
{ color: "#ff9f43", label: "高危事件" },
|
||||
{ color: "#ffd166", label: "中危事件" },
|
||||
{ color: "#4dabf7", label: "低危事件" },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export const PATHS = {
|
||||
cablesApi: '/api/v1/visualization/geo/cables',
|
||||
landingPointsApi: '/api/v1/visualization/geo/landing-points',
|
||||
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
|
||||
bgpIncidentsApi: '/api/v1/visualization/geo/bgp-incidents',
|
||||
bgpCollectorsApi: '/api/v1/visualization/geo/bgp-collectors',
|
||||
geoJSON: './geo.json',
|
||||
landingPointsStatic: './landing-point-geo.geojson',
|
||||
|
||||
@@ -32,23 +32,23 @@ const CARD_CONFIG = {
|
||||
},
|
||||
bgp: {
|
||||
icon: '📡',
|
||||
title: 'BGP异常详情',
|
||||
title: 'BGP事件详情',
|
||||
className: 'bgp',
|
||||
fields: [
|
||||
{ key: 'anomaly_type', label: '异常类型' },
|
||||
{ key: 'anomaly_type', label: '事件类型' },
|
||||
{ key: 'severity', label: '严重度' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'route_change', label: '路由变更' },
|
||||
{ key: 'route_change', label: '事件特征' },
|
||||
{ key: 'prefix', label: '前缀' },
|
||||
{ key: 'as_path_display', label: '传播路径' },
|
||||
{ key: 'origin_asn', label: '原始 ASN' },
|
||||
{ key: 'new_origin_asn', label: '新 ASN' },
|
||||
{ key: 'origin_asn', label: '涉及 ASN' },
|
||||
{ key: 'new_origin_asn', label: '关联 ASN' },
|
||||
{ key: 'confidence', label: '置信度' },
|
||||
{ key: 'collector', label: '采集器' },
|
||||
{ key: 'collector', label: '主观测站' },
|
||||
{ key: 'observed_by', label: '观测范围' },
|
||||
{ key: 'impacted_scope', label: '影响区域' },
|
||||
{ key: 'location', label: '观测位置' },
|
||||
{ key: 'created_at', label: '发生时间' },
|
||||
{ key: 'created_at', label: '事件时间' },
|
||||
{ key: 'summary', label: '摘要' }
|
||||
]
|
||||
},
|
||||
@@ -59,7 +59,7 @@ const CARD_CONFIG = {
|
||||
fields: [
|
||||
{ key: 'collector', label: '采集器' },
|
||||
{ key: 'location', label: '观测位置' },
|
||||
{ key: 'anomaly_count', label: '当前异常数' },
|
||||
{ key: 'anomaly_count', label: '当前事件数' },
|
||||
{ key: 'status', label: '状态' }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -72,6 +72,8 @@ import {
|
||||
getBGPCollectorMarkers,
|
||||
getBGPLegendItems,
|
||||
getBGPCount,
|
||||
getBGPCollectorCount,
|
||||
getBGPStatusSummary,
|
||||
getShowBGP,
|
||||
clearBGPSelection,
|
||||
setBGPMarkerState,
|
||||
@@ -382,24 +384,44 @@ function showBGPInfo(marker) {
|
||||
},
|
||||
];
|
||||
showInfoCard("bgp", {
|
||||
anomaly_type: formatBGPAnomalyTypeLabel(marker.userData.anomaly_type),
|
||||
anomaly_type: formatBGPAnomalyTypeLabel(
|
||||
marker.userData.incident_type || marker.userData.anomaly_type,
|
||||
),
|
||||
severity: formatBGPSeverityLabel(
|
||||
marker.userData.rawSeverity || marker.userData.severity,
|
||||
),
|
||||
status: formatBGPStatusLabel(marker.userData.status),
|
||||
route_change: formatBGPRouteChange(
|
||||
marker.userData.origin_asn,
|
||||
marker.userData.new_origin_asn,
|
||||
),
|
||||
prefix: marker.userData.prefix,
|
||||
as_path_display: formatBGPASPath(marker.userData.as_path),
|
||||
origin_asn: marker.userData.origin_asn,
|
||||
new_origin_asn: marker.userData.new_origin_asn,
|
||||
route_change:
|
||||
marker.userData.route_change ||
|
||||
formatBGPRouteChange(
|
||||
marker.userData.origin_asn,
|
||||
marker.userData.new_origin_asn,
|
||||
),
|
||||
prefix:
|
||||
Array.isArray(marker.userData.prefixes) && marker.userData.prefixes.length > 1
|
||||
? `${marker.userData.prefixes[0]} 等${marker.userData.prefixes.length}个`
|
||||
: marker.userData.prefix,
|
||||
as_path_display:
|
||||
Array.isArray(marker.userData.as_path) && marker.userData.as_path.length > 0
|
||||
? formatBGPASPath(marker.userData.as_path)
|
||||
: "-",
|
||||
origin_asn:
|
||||
Array.isArray(marker.userData.affected_asns) && marker.userData.affected_asns.length > 0
|
||||
? marker.userData.affected_asns.slice(0, 3).map((asn) => `AS${asn}`).join(", ")
|
||||
: marker.userData.origin_asn,
|
||||
new_origin_asn:
|
||||
Array.isArray(marker.userData.affected_asns) && marker.userData.affected_asns.length > 3
|
||||
? `共${marker.userData.affected_asns.length}个ASN`
|
||||
: marker.userData.new_origin_asn,
|
||||
confidence: formatBGPConfidence(marker.userData.confidence),
|
||||
collector: marker.userData.collector,
|
||||
observed_by: formatBGPObservedBy(marker.userData.collectors),
|
||||
observed_by:
|
||||
marker.userData.observed_by ||
|
||||
formatBGPObservedBy(marker.userData.collectors),
|
||||
impacted_scope: formatBGPImpactedScope(impactedRegions),
|
||||
location: formatBGPLocation(marker.userData.city, marker.userData.country),
|
||||
location:
|
||||
marker.userData.location ||
|
||||
formatBGPLocation(marker.userData.city, marker.userData.country),
|
||||
created_at: formatBGPObservedTime(marker.userData.created_at_raw),
|
||||
summary: marker.userData.summary,
|
||||
});
|
||||
@@ -603,6 +625,8 @@ function updateStatsSummary() {
|
||||
landingPointCount:
|
||||
document.getElementById("landing-point-count")?.textContent || 0,
|
||||
bgpAnomalyCount: `${getBGPCount()} 条`,
|
||||
bgpCollectorCount: `${getBGPCollectorCount()} 个`,
|
||||
bgpStatusSummary: getBGPStatusSummary(),
|
||||
terrainOn: getShowTerrain(),
|
||||
textureQuality: "8K 卫星图",
|
||||
});
|
||||
@@ -717,8 +741,8 @@ async function loadData(showWhiteSphere = false) {
|
||||
setLoadingMessage(
|
||||
showWhiteSphere ? "正在刷新全球态势数据..." : "正在初始化全球态势数据...",
|
||||
showWhiteSphere
|
||||
? "重新同步卫星、海底光缆、登陆点与BGP异常数据"
|
||||
: "同步卫星、海底光缆、登陆点与BGP异常数据",
|
||||
? "重新同步卫星、海底光缆、登陆点与BGP态势数据"
|
||||
: "同步卫星、海底光缆、登陆点与BGP态势数据",
|
||||
);
|
||||
setLoading(true);
|
||||
clearLockedObject();
|
||||
@@ -746,7 +770,20 @@ async function loadData(showWhiteSphere = false) {
|
||||
}
|
||||
const bgpCountEl = document.getElementById("bgp-anomaly-count");
|
||||
if (bgpCountEl) {
|
||||
bgpCountEl.textContent = `${bgpResult.totalCount} 条`;
|
||||
bgpCountEl.textContent = `${bgpResult.totalCount} 起`;
|
||||
}
|
||||
const bgpCollectorEl = document.getElementById("bgp-collector-count");
|
||||
if (bgpCollectorEl) {
|
||||
bgpCollectorEl.textContent = `${bgpResult.collectorCount} 个`;
|
||||
}
|
||||
const bgpStatusEl = document.getElementById("bgp-status-summary");
|
||||
if (bgpStatusEl) {
|
||||
bgpStatusEl.textContent =
|
||||
bgpResult.totalCount > 0
|
||||
? `${bgpResult.totalCount} 起活跃事件`
|
||||
: bgpResult.anomalyCount > 0
|
||||
? `${bgpResult.anomalyCount} 条活跃异常`
|
||||
: "当前无活跃事件";
|
||||
}
|
||||
return bgpResult;
|
||||
})(),
|
||||
@@ -765,7 +802,7 @@ async function loadData(showWhiteSphere = false) {
|
||||
errors.push({ label: "卫星", reason: results[1].reason });
|
||||
}
|
||||
if (results[2].status === "rejected") {
|
||||
errors.push({ label: "BGP异常", reason: results[2].reason });
|
||||
errors.push({ label: "BGP态势", reason: results[2].reason });
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
@@ -1175,7 +1212,7 @@ function onClick(event) {
|
||||
showBGPEventOverlay(clickedMarker, earth);
|
||||
showBGPInfo(clickedMarker);
|
||||
showStatusMessage(
|
||||
`已选择BGP异常: ${clickedMarker.userData.collector}`,
|
||||
`已选择BGP事件: ${clickedMarker.userData.collector}`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -86,6 +86,8 @@ export function updateEarthStats(stats) {
|
||||
const cableCountEl = document.getElementById("cable-count");
|
||||
const landingPointCountEl = document.getElementById("landing-point-count");
|
||||
const bgpAnomalyCountEl = document.getElementById("bgp-anomaly-count");
|
||||
const bgpCollectorCountEl = document.getElementById("bgp-collector-count");
|
||||
const bgpStatusSummaryEl = document.getElementById("bgp-status-summary");
|
||||
const terrainStatusEl = document.getElementById("terrain-status");
|
||||
const textureQualityEl = document.getElementById("texture-quality");
|
||||
|
||||
@@ -94,6 +96,10 @@ export function updateEarthStats(stats) {
|
||||
landingPointCountEl.textContent = stats.landingPointCount || 0;
|
||||
if (bgpAnomalyCountEl)
|
||||
bgpAnomalyCountEl.textContent = stats.bgpAnomalyCount || 0;
|
||||
if (bgpCollectorCountEl)
|
||||
bgpCollectorCountEl.textContent = stats.bgpCollectorCount || 0;
|
||||
if (bgpStatusSummaryEl)
|
||||
bgpStatusSummaryEl.textContent = stats.bgpStatusSummary || "-";
|
||||
if (terrainStatusEl)
|
||||
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
|
||||
if (textureQualityEl)
|
||||
|
||||
@@ -20,6 +20,32 @@ interface BGPAnomaly {
|
||||
created_at: string | null
|
||||
}
|
||||
|
||||
interface BGPEvent {
|
||||
id: number
|
||||
collector: string | null
|
||||
event_type: string
|
||||
prefix: string | null
|
||||
origin_asn: number | null
|
||||
peer_asn: number | null
|
||||
observed_at: string | null
|
||||
}
|
||||
|
||||
interface BGPIncident {
|
||||
id: number
|
||||
incident_type: string
|
||||
title: string
|
||||
summary: string
|
||||
severity: string
|
||||
status: string
|
||||
confidence: number
|
||||
affected_prefixes: string[]
|
||||
affected_asns: number[]
|
||||
affected_collectors: string[]
|
||||
affected_regions: Array<{ country?: string; city?: string }>
|
||||
created_at: string | null
|
||||
started_at: string | null
|
||||
}
|
||||
|
||||
interface Summary {
|
||||
total: number
|
||||
by_type: Record<string, number>
|
||||
@@ -27,6 +53,13 @@ interface Summary {
|
||||
by_status: Record<string, number>
|
||||
}
|
||||
|
||||
interface EventSummary {
|
||||
total: number
|
||||
collector_count: number
|
||||
prefix_count: number
|
||||
by_type: Record<string, number>
|
||||
}
|
||||
|
||||
function severityColor(severity: string) {
|
||||
if (severity === 'critical') return 'red'
|
||||
if (severity === 'high') return 'orange'
|
||||
@@ -36,19 +69,31 @@ function severityColor(severity: string) {
|
||||
|
||||
function BGP() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [incidents, setIncidents] = useState<BGPIncident[]>([])
|
||||
const [anomalies, setAnomalies] = useState<BGPAnomaly[]>([])
|
||||
const [summary, setSummary] = useState<Summary | null>(null)
|
||||
const [events, setEvents] = useState<BGPEvent[]>([])
|
||||
const [incidentSummary, setIncidentSummary] = useState<Summary | null>(null)
|
||||
const [anomalySummary, setAnomalySummary] = useState<Summary | null>(null)
|
||||
const [eventSummary, setEventSummary] = useState<EventSummary | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [anomaliesRes, summaryRes] = await Promise.all([
|
||||
const [incidentsRes, incidentSummaryRes, anomaliesRes, anomalySummaryRes, eventsRes, eventSummaryRes] = await Promise.all([
|
||||
axios.get('/api/v1/bgp/incidents', { params: { page_size: 50 } }),
|
||||
axios.get('/api/v1/bgp/incidents/summary'),
|
||||
axios.get('/api/v1/bgp/anomalies', { params: { page_size: 100 } }),
|
||||
axios.get('/api/v1/bgp/anomalies/summary'),
|
||||
axios.get('/api/v1/bgp/events', { params: { page_size: 20 } }),
|
||||
axios.get('/api/v1/bgp/events/summary'),
|
||||
])
|
||||
setIncidents(incidentsRes.data.data || [])
|
||||
setIncidentSummary(incidentSummaryRes.data)
|
||||
setAnomalies(anomaliesRes.data.data || [])
|
||||
setSummary(summaryRes.data)
|
||||
setAnomalySummary(anomalySummaryRes.data)
|
||||
setEvents(eventsRes.data.data || [])
|
||||
setEventSummary(eventSummaryRes.data)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -62,7 +107,7 @@ function BGP() {
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Title level={3} style={{ marginBottom: 4 }}>BGP观测</Title>
|
||||
<Text type="secondary">查看实时与回放阶段归一化出的路由异常。</Text>
|
||||
<Text type="secondary">先看事件态势,再下钻到原子异常明细。</Text>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
@@ -74,22 +119,102 @@ function BGP() {
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}>
|
||||
<Card>
|
||||
<Statistic title="异常总数" value={summary?.total || 0} />
|
||||
<Statistic title="观测事件" value={eventSummary?.total || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card>
|
||||
<Statistic title="Critical" value={summary?.by_severity?.critical || 0} />
|
||||
<Statistic title="观测站" value={eventSummary?.collector_count || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card>
|
||||
<Statistic title="Active" value={summary?.by_status?.active || 0} />
|
||||
<Statistic title="观测前缀" value={eventSummary?.prefix_count || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="异常列表">
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}>
|
||||
<Card>
|
||||
<Statistic title="事件总数" value={incidentSummary?.total || 0} />
|
||||
</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<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: 'confidence',
|
||||
width: 120,
|
||||
render: (value: number) => `${Math.round((value || 0) * 100)}%`,
|
||||
},
|
||||
{
|
||||
title: '摘要',
|
||||
dataIndex: 'summary',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="异常明细">
|
||||
<Table<BGPAnomaly>
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
@@ -151,6 +276,52 @@ function BGP() {
|
||||
]}
|
||||
/>
|
||||
</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>
|
||||
</AppLayout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user