feat(display): add compact QUIC and host health views
This commit is contained in:
1 parent
b964dc2bbc
commit
b25b111134
14 files changed
+921
-251
No files matched your search
@@ -19,52 +19,6 @@ const levelOf = (loss: number | null | undefined): Level => (loss === null || lo
|
||||
|
||||
const clock = (ms: number) => new Date(ms).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
|
||||
interface StripProps {
|
||||
/** 左上:什么延迟(UDP 延迟 p50)。 */
|
||||
rttLabel: string
|
||||
rtt: number | null | undefined
|
||||
/** 右上:丢包还是重传。 */
|
||||
badLabel: string
|
||||
bad: number | null | undefined
|
||||
badHint?: string
|
||||
/** 每 5 分钟一格:这一格的丢包(重传)率、延迟。 */
|
||||
t: number[]
|
||||
badSeries: (number | null)[]
|
||||
ready: boolean
|
||||
/** 提示里这一格的「丢包」叫什么。 */
|
||||
tipBad: string
|
||||
/** 没有数据时占位的格数,和每格长度的说明(「每 5 分钟」)。 */
|
||||
cells: number
|
||||
stepText: string
|
||||
hoursText: string
|
||||
/** 悬停的那一格(UDP 和 TCP 共用);`null` = 没有。 */
|
||||
hover: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 一排小格(每 5 分钟一格,按这 5 分钟的丢包率着色:绿 / 黄 / 红 / 灰 = 没有数据)
|
||||
* 尽量轻。
|
||||
*/
|
||||
function Strip({ rttLabel, rtt, badLabel, bad, badHint, t, badSeries, ready, cells, stepText, hoursText, hover }: StripProps) {
|
||||
return (
|
||||
<div className="udp-strip">
|
||||
<div className="udp-head">
|
||||
<span className="udp-title">
|
||||
{rttLabel} <b className="num">{fmtMs(rtt)}</b>
|
||||
</span>
|
||||
<span className="udp-title" title={badHint}>
|
||||
{badLabel} <b className={`udp-rate num ${levelOf(bad)}`}>{fmtPct(bad)}</b>
|
||||
</span>
|
||||
</div>
|
||||
<div className="bars" role="img" aria-label={`近 ${hoursText}每 ${stepText}的${badLabel}情况`}>
|
||||
{(ready ? badSeries : Array.from({ length: cells }, () => null)).map((v, i) => (
|
||||
<span key={t[i] ?? i} className={`bar ${ready ? levelOf(v) : 'none'}${hover === i ? ' on' : ''}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
lineId: string
|
||||
range: OverviewRange
|
||||
@@ -72,23 +26,23 @@ interface Props {
|
||||
m: Record<string, number | null | undefined>
|
||||
}
|
||||
|
||||
/** 一条线路近一段时间的 UDP 和 TCP:各是一排小格,共用一次请求。 */
|
||||
/** 一条线路的 QUIC/TCP 双行摘要,各自显示时延、质量点阵和丢包/重传率。 */
|
||||
export function LineStrips({ lineId, range, m }: Props) {
|
||||
const info = OVERVIEW_RANGES.find((r) => r.value === range) ?? OVERVIEW_RANGES[0]
|
||||
const series = useQuery(
|
||||
() => api.get<SeriesDto>(`/api/public/lines/${encodeURIComponent(lineId)}/series`, { range: info.value, anchor: 'latest', step: info.step, preset: 'udp,tcp', metrics: 'loss.total,rtt.p50,tcp.retrans,tcp.rtt.p50' }),
|
||||
() => api.get<SeriesDto>(`/api/public/lines/${encodeURIComponent(lineId)}/series`, { range: info.value, anchor: 'latest', step: info.step, preset: 'quic,tcp', metrics: 'loss.up,loss.down,loss.total,rtt.p50,rtt.p95,rtt.p99,tcp.retrans,tcp.rtt.p50' }),
|
||||
[lineId, info.value],
|
||||
30_000,
|
||||
)
|
||||
const s = series.data
|
||||
const t = s?.t ?? []
|
||||
const col = (key: string): (number | null)[] => s?.values[key] ?? []
|
||||
// 悬停的那一格:UDP 和 TCP 的时间轴一样,所以同一格同时高亮;提示弹在卡片右侧,一次展示这段时间的 UDP 和 TCP
|
||||
// 悬停的那一格:QUIC 上下行和 TCP 共用时间轴。
|
||||
const [hover, setHover] = useState<number | null>(null)
|
||||
const last = useRef<number | null>(null) // 提示收回去的动画期间,内容还要留着
|
||||
if (hover !== null) last.current = hover
|
||||
const shown = hover ?? last.current
|
||||
// 整块线路区域都能触发:按鼠标横向位置对应到小格(UDP 和 TCP 的小格一样宽,量第一排就行),不用精确指到小格上
|
||||
// 整块线路区域都能触发:按鼠标横向位置对应到采样格,不用精确指到小格上。
|
||||
const box = useRef<HTMLDivElement>(null)
|
||||
const onMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
const bars = box.current?.querySelector('.bars')
|
||||
@@ -98,19 +52,50 @@ export function LineStrips({ lineId, range, m }: Props) {
|
||||
}
|
||||
const common = { t, ready: Boolean(s), cells: Math.round((info.hours * 60) / info.stepMin), stepText: `${info.stepMin} 分钟`, hoursText: `${info.hours} 小时`, hover }
|
||||
const at = shown !== null && t[shown] !== undefined ? shown : null
|
||||
const lossUp = col('loss.up')
|
||||
const lossDown = col('loss.down')
|
||||
const retrans = col('tcp.retrans')
|
||||
const up = m['loss.up']
|
||||
const down = m['loss.down']
|
||||
const upText = up === null || up === undefined ? '丢包 —' : `${fmtPct(up)} 丢包`
|
||||
const downText = down === null || down === undefined ? '丢包 —' : `${fmtPct(down)} 丢包`
|
||||
const retransValue = m['tcp.retrans']
|
||||
const retransTone = levelOf(retransValue)
|
||||
const retransText = retransValue === null || retransValue === undefined ? '重传 —' : `${fmtPct(retransValue)} 重传`
|
||||
return (
|
||||
<div className="strips" ref={box} onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
|
||||
<Strip
|
||||
{...common}
|
||||
rttLabel="UDP 延迟 p50"
|
||||
rtt={m['rtt.p50']}
|
||||
badLabel="丢包"
|
||||
bad={m['loss.total']}
|
||||
badHint={`上行丢包 ${fmtPct(m['loss.up'])} 下行丢包 ${fmtPct(m['loss.down'])}`}
|
||||
badSeries={col('loss.total')}
|
||||
tipBad="丢包"
|
||||
/>
|
||||
<Strip {...common} rttLabel="TCP RTT p50" rtt={m['tcp.rtt.p50']} badLabel="重传" bad={m['tcp.retrans']} badSeries={col('tcp.retrans')} tipBad="重传" />
|
||||
<div className="line-mini-stack">
|
||||
<div className="line-mini-quic-head" title={`QUIC RTT p50 ${fmtMs(m['rtt.p50'])} · p95 ${fmtMs(m['rtt.p95'])} · p99 ${fmtMs(m['rtt.p99'])}`}>
|
||||
<span>QUIC</span><strong className="num">{fmtMs(m['rtt.p50'])}</strong><small>悬停看 p95 / p99</small>
|
||||
</div>
|
||||
<div className="line-mini-direction-row" title={`去程 Probe → Agent 丢包 ${fmtPct(up)}`}>
|
||||
<span>去程</span>
|
||||
<div className="bars" role="img" aria-label={`近 ${common.hoursText}每 ${common.stepText}的去程丢包`}>
|
||||
{(s ? lossUp : Array.from({ length: common.cells }, () => null)).map((value, i) => (
|
||||
<span key={`up-${t[i] ?? i}`} className={`bar ${s ? levelOf(value) : 'none'}${hover === i ? ' on' : ''}`} />
|
||||
))}
|
||||
</div>
|
||||
<span className={`line-mini-badge ${levelOf(up)}`}>{upText}</span>
|
||||
</div>
|
||||
<div className="line-mini-direction-row" title={`回程 Agent → Probe 丢包 ${fmtPct(down)}`}>
|
||||
<span>回程</span>
|
||||
<div className="bars" role="img" aria-label={`近 ${common.hoursText}每 ${common.stepText}的回程丢包`}>
|
||||
{(s ? lossDown : Array.from({ length: common.cells }, () => null)).map((value, i) => (
|
||||
<span key={`down-${t[i] ?? i}`} className={`bar ${s ? levelOf(value) : 'none'}${hover === i ? ' on' : ''}`} />
|
||||
))}
|
||||
</div>
|
||||
<span className={`line-mini-badge ${levelOf(down)}`}>{downText}</span>
|
||||
</div>
|
||||
<div className="line-mini-tcp-row" title={`TCP RTT p50 ${fmtMs(m['tcp.rtt.p50'])}`}>
|
||||
<span>TCP</span><strong className="num">{fmtMs(m['tcp.rtt.p50'])}</strong>
|
||||
<div className="bars" role="img" aria-label={`近 ${common.hoursText}每 ${common.stepText}的 TCP 重传`}>
|
||||
{(s ? retrans : Array.from({ length: common.cells }, () => null)).map((value, i) => (
|
||||
<span key={`retrans-${t[i] ?? i}`} className={`bar ${s ? levelOf(value) : 'none'}${hover === i ? ' on' : ''}`} />
|
||||
))}
|
||||
</div>
|
||||
<span className={`line-mini-badge ${retransTone}`}>{retransText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`pop num${hover !== null && at !== null ? ' open' : ''}`} role="tooltip" aria-hidden={hover === null}>
|
||||
{at !== null ? (
|
||||
<>
|
||||
@@ -118,12 +103,15 @@ export function LineStrips({ lineId, range, m }: Props) {
|
||||
{clock(t[at])}–{clock(t[at] + info.stepMin * 60_000)}
|
||||
</b>
|
||||
<div className="pop-group">
|
||||
<span className="pop-name">UDP</span>
|
||||
<span className="pop-name">QUIC Datagram</span>
|
||||
<span>RTT p50 <em>{fmtMs(col('rtt.p50')[at])}</em></span>
|
||||
<span>RTT p95 <em>{fmtMs(col('rtt.p95')[at])}</em></span>
|
||||
<span>RTT p99 <em>{fmtMs(col('rtt.p99')[at])}</em></span>
|
||||
<span>
|
||||
丢包 <em className={levelOf(col('loss.total')[at])}>{fmtPct(col('loss.total')[at])}</em>
|
||||
去程丢包 <em className={levelOf(col('loss.up')[at])}>{fmtPct(col('loss.up')[at])}</em>
|
||||
</span>
|
||||
<span>
|
||||
延迟 p50 <em>{fmtMs(col('rtt.p50')[at])}</em>
|
||||
回程丢包 <em className={levelOf(col('loss.down')[at])}>{fmtPct(col('loss.down')[at])}</em>
|
||||
</span>
|
||||
</div>
|
||||
<div className="pop-group">
|
||||
|
||||
Reference in new issue
Block a user