外观
复合块:看板、抖动可视化、机群地图
ui/boards/、ui/flaps/、ui/map/ 三层。共同点:整块区域、由 data prop 驱动、 自己不取数、不做轮询——所以同一个块既能挂在控制台,也能挂在只读的门户上。
字段口径以 接口契约 为准;本文只讲组件侧的 API 与渲染约定。
boards —— 仪表盘区块
FleetTraffic
流量看板:一个 range 统辖一组互补的卡片——hero(相对变化 + 构成环 + 双 Top-5)、 唯一的绝对值趋势卡、以及按对端 AS 的模块。每个事实只陈述一次。
| prop | 类型 | 说明 |
|---|---|---|
points? | TrafficPoint[] | traffic.points,定长桶网格,空桶为 null(缺报,不是 0) |
pointsPrevious? | TrafficPoint[] | traffic.points_previous,紧邻上一窗口,同网格按下标对齐 |
breakdown? | FleetTrafficBreakdown | null | 当前每节点 / 每对端速率榜;这里只用 nodes 的 Top-5 |
asTraffic? | TrafficByAs | null | 透传给内嵌的 AsTraffic |
trafficMix? | TrafficMix | null | 内部/外部 · 收/发 双半环 |
names? | Map<number, string> | ASN → registry as-name 的补充映射;命中即用,否则回落后端已解析的名字 |
loaded? | boolean | 首次取数是否已返回——只在此之前画骨架,避免闪占位内容 |
asof? | string | — |
ranges | readonly string[] | 必填,由宿主给:管理面 24h/7d/30d,门户只有 6h/24h |
range | string | 当前档(读) |
onRangeChange? | (v) => void | 当前档(写)。读写分离,因为控制台把它绑在 URL 上 |
nodeHref? | (nodeId) => string | undefined | 节点详情链接。不传则 Top-5 里的节点渲染成纯文本——门户没有节点详情页,渲染成死链比不渲染更糟 |
ranges 必须由宿主给:后端不把长时段下放到全网口径,写死在组件里必有一边是错的。
「流量相对变化」是前端算的(从 pointsPrevious 逐桶求百分比),没有对应的后端字段。
AsTraffic
Radar 的「按自治系统划分的网络流量」模块:左侧 Top-5 对端 AS 的流量趋势多线图 (图例带各自份额),右侧 Top-N 份额表 + 分页。
| prop | 类型 | 说明 |
|---|---|---|
data? | TrafficByAs | null | dashboard.traffic_by_as |
loaded? | boolean | 门控首屏骨架 |
names? | Map<number, string> | ASN → registry as-name(权威,优先于载荷里的 name,后者可能是 peering 的本地叫法) |
asof? | string | — |
loaded 与 data 分开的理由:在首次取数返回之前渲染的是镜像真实两栏版式的骨架, 而不是任何占位内容;返回之后若载荷缺失或为空,才给正向空状态。两种「没有」要区分开。
FleetRouting
全机群路由概览。版式的选择本身就是一个判断:
iBGP 让每个节点收敛到几乎相同的 RIB,所以画每节点路由数是冗余的(线全叠在一起)。 因此左侧画机群整体的路由表规模趋势(一条线,节点已收敛,中位数即代表机群)与 路由变化量;右侧用节点列表呈现真正逐节点不同的东西:收敛状态(数量对不上 = iBGP / 收敛问题)与 RPKI-invalid 计数(随各节点的外部对端而不同)。
ts
data?: FleetRoutingOverview | null; // 随宿主的 /ui/dashboard 一起取,null = 尚未加载
asof?: string;FlapStatsPanel
抖动活动 hero:900s 截尾均值速率表盘 + 路由变化量图 + 活跃前缀数图,共用固定桶网格。
| prop | 类型 | 说明 |
|---|---|---|
data? | FleetFlapStats | null | /fleet/flap-stats 响应 |
error? | string | 取数失败的文案;空即无错 |
obsNodes? | number | null | 观测节点数(feed_state='established' 的条数)。null → 隐藏换算开关,表盘显示原始和 |
range? | string ⇄ | 时间窗。类型是 string 而非联合类型,因为控制台直接 bind 到 URL 的 ?range=;组件内按白名单校验,非法值回落 '1h' |
scale? | string ⇄ | 表盘口径:'fleet' 原始和 / 'node' 除以观测节点数 |
obsNodes 为什么需要:速率是跨节点求和,一次 churn 在每个看到它的节点各计一次, 直接读会比 FlapAlerted 这类单视角工具虚高。有这个数才能换算成每节点口径。
配色是有意的:数量序列走数据色;「已告警」是最严重的一层,占用状态红;「抖动中」 降级为中性数据色——状态色是稀缺资源,同一张图里两条线都用红会让人分不清哪条才是要处理的。
gauge 不随 range 变(固定 900s 窗口),所以表盘与时序图共用同一个响应。
flaps —— 抖动可视化
这一组的核心是把「该怪谁」画出来。四个根因区(FLAP_ZONES)按多大程度上该由自己 处理排序,这个顺序在分布磁贴、筛选 chips 和徽章之间共享:
| 区 | 含义 | 可采取的动作 |
|---|---|---|
near | 近端对端在抖 | 可以断开该对端 |
origin | 起源 AS 在抖 | 通知对方 / 拒收前缀 |
path | 路径中某条邻接断裂 | 观察,不宜处置 |
moas | 多源(疑似劫持) | 绝不动手,先核实 |
区的颜色是各组件里的 .z-{zone} 类;文案在 i18n 的 pflap.zone.* / pflap.rc.act.*。
PrefixFlapBoard
前缀级抖动看板——完整问题清单,不是 Top-N 榜。以前缀为主体聚合,每条一行, 展开是它的 per-peer 抽屉。
| prop | 类型 | 说明 |
|---|---|---|
data? | FleetPrefixFlaps | FleetPrefixFlapsGrouped | null | ?group=prefix 响应;分组形态自带内嵌的根因定位 |
error? | string | 取数失败文案 |
rowCap? | number | 取数时用的行数上限,取数方传了什么就填什么——用来判断结果是否被截断并给提示 |
pageSize? | number | 默认 50:一屏滚一下能看完,rowCap 500 时正好 10 页 |
prefixActions? | Snippet<[GroupedPrefixRow, PrefixFlapLocalizationCore | null | undefined]> | 前缀行的行内动作 |
peerActions? | Snippet<[GroupedPrefixPeer]> | 抽屉内 per-peer 行的行内动作 |
版式为什么这样:同一条前缀若经很多个对端抖进来,说明 churn 起于源头 AS 再沿 网状扩散,该看的是源头而不是其中任何一个对端——「对端数」与「源 AS」两列因此并排放。 MOAS 单独标红,它是另一类异常。
取数方注意:保留服务端默认的 min_score 下限,不要传 0。那会把每条已平息的 前缀连同它的全部 peers × nodes 一起拉回来,payload 大到能冻住标签页,而那些行本来就 没有根因可看。
分数由服务端持续衰减到当前时刻,所以平息下来的行会随轮询自己滑走,前端不需要做任何清理。
不传两个 action snippet 即整列为空——门户是只读的,处置属于管理面的写路径。
PrefixFlapTable
扁平行表:一行一个 (前缀, 归因对端)。两个使用场景共用:PrefixFlapBoard 在拿到扁平 形态时的回退渲染,以及单对端详情页(它的 prefixes[] 正是这个形状)。
ts
rows: PrefixFlapRow[];
showPeer?: boolean; // 每行同属一个对端时隐藏「归因对端」列Rate / Duration 两列在没有任何一行带这些字段时自动隐藏。
RootCauseBadge
根因区徽章——「根因」列的头条。
ts
zone: FlapZone;
confidence: number; // 0..1颜色编码区,但区名永远写在旁边(颜色不单独承载信息);一条细计量条承载置信度。 低置信度的定位会把整个徽章淡化,这样一个动摇的猜测不会盖过一个扎实的。
置信度刻意不做成醒目的大数字——它是修饰,不是结论。
RootCauseSummary
只读的 fleet 根因构成条,数据来自 localization_summary。
ts
counts: FlapZoneCounts;它的数字与下方的筛选 chips 不是同一组,这是有意的:localization_summary 计的是 全网每一条抖动前缀(含被分页上限截断的部分),chips 计的是当前可见行。因此它做成静态 比例条 + 图例,永不可点——给全局真相,又不让筛选数字对不上。
CulpritRef
嫌疑点的紧凑形态,落在「根因」列徽章旁,按区渲染:
| 区 | 渲染 |
|---|---|
near / origin | 单个嫌疑 AS(经 AsLink 带 registry 名) |
path | churn 断裂的那条邻接:A ✕ B |
moas | 竞争源的 chip 列表 |
ts
loc: PrefixFlapLocalizationCore;
origins?: PrefixFlapOriginRef[]; // 行的权威竞争源集合纯展示——近端的「断开」动作留在看板里,这样它保持可复用。
AsPathStrip
抽屉里的招牌视图:「原来是断在这里」。从近端到起源的横向 AS chip 条,嫌疑段用区 颜色点亮。
near: [●peer]──…──[origin] 第一跳点亮
origin: [peer]──…──[●origin] 最后一跳点亮
path: [peer]──[A]✕[B]──[origin] 断裂邻接点亮
moas: [peer]──…──[●origin] + 其他源ts
loc: PrefixFlapLocalizationCore;
origins?: PrefixFlapOriginRef[];完整条需要后端的 representative_path;缺失时降级成只画 culprit_edge(A ✕ B)或 单个嫌疑 chip——仍然是有用的那部分。确实没什么可画时渲染为空。
MOAS 画得很诚实:representative_path 是通往某一个起源的一条(churn 最高) 路径,所以画成一条止于那个起源的线,再把其他竞争源列在旁边——不是从最后一跳 分叉(它们并不都经过那一跳,而可供分叉的拓扑本来就不在数据里)。
map —— 机群地图
FleetMap
地图 + 节点列表的组合体。宽屏下左 60% 是可平移缩放的世界地图,右 40% 是与地图联动的 紧凑节点列表。
| prop | 类型 | 说明 |
|---|---|---|
nodes | FleetMapNode[] | — |
links? | FleetLink[] | WG 骨干链路,画成大圆弧 |
fitWorld? | boolean | 初始视图适配全世界 |
onRequestSnapshot? | (nodeId) => void | Promise<void> | 传了才渲染这个按钮。报错与 toast 归调用方——库不认识 api,也没有 toast 出口 |
health? | string ⇄ | 健康度筛选值('' = 不筛)。控制台绑到 URL 的 ?health=;只读场景不传,组件用内部状态 |
投影:太平洋居中等距圆柱,W=1000 H=500 LON0=165。这三个数在三处必须一致—— 本组件、首页的内联脚本、apps/home/bake.js。不一致会在首页换层时看到明显跳变。
聚簇:标记按 DN42 region → 国家 → 城市三级聚簇,缩放跨过阈值时把一簇拆成单个节点。 簇的位置取组内节点坐标的均值,颜色取组内最差健康度。
坐标来自服务端。 site → 坐标 / 城市 / 国家 / region 的注册表在控制服务器上, 每个节点行自带解析好的 geo——加一个机房不再需要发一版前端。geo.ts 里剩下的只是 展示逻辑。
geo.ts
| 导出 | 说明 |
|---|---|
REGION_NAMES | DN42 origin-region community(41..57)→ 英文名。协议标准,静态 |
RegionCode | number(41..57) |
ResolvedGeo | 聚簇/标签代码消费的形状 |
fromNodeGeo(geo, site?) | 把服务端的 NodeGeo 适配成 ResolvedGeo |
geoLabel(geo, site?) | 渲染成显示用的地点标签 |
国家名走 Intl.DisplayNames 但钉死英文——与英文的城市名、region 名保持一致, 不跟随运行时语言。单城市辖区(特别行政区等)在地图/列表树里跳过冗余的城市层级。