feat(display): explain QUIC state and compact panels [CI]
This commit is contained in:
1 parent
efb50d0efe
commit
85c2043f2f
4 files changed
+96
-52
No files matched your search
@@ -6,9 +6,9 @@ import { useQuery } from '@shared/hooks'
|
||||
|
||||
/** 总览卡片可以看的时长,以及每一格的长度(格数约 36~48,太多就挤了)。 */
|
||||
export const OVERVIEW_RANGES = [
|
||||
{ value: '3h', label: '3 小时', hours: 3, step: '5m', stepMin: 5 },
|
||||
{ value: '6h', label: '6 小时', hours: 6, step: '10m', stepMin: 10 },
|
||||
{ value: '12h', label: '12 小时', hours: 12, step: '15m', stepMin: 15 },
|
||||
{ value: '3h', label: '近 3 小时', hours: 3, step: '5m', stepMin: 5 },
|
||||
{ value: '6h', label: '近 6 小时', hours: 6, step: '10m', stepMin: 10 },
|
||||
{ value: '12h', label: '近 12 小时', hours: 12, step: '15m', stepMin: 15 },
|
||||
] as const
|
||||
|
||||
export type OverviewRange = (typeof OVERVIEW_RANGES)[number]['value']
|
||||
@@ -68,32 +68,38 @@ export function LineStrips({ lineId, range, m }: Props) {
|
||||
<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)}`}>
|
||||
<div className="line-mini-direction">
|
||||
<div className="line-mini-direction-head" title={`去程 Probe → Agent 丢包 ${fmtPct(up)}`}>
|
||||
<span>去程</span>
|
||||
<span className={`line-mini-badge ${levelOf(up)}`}>{upText}</span>
|
||||
</div>
|
||||
<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)}`}>
|
||||
<div className="line-mini-direction">
|
||||
<div className="line-mini-direction-head" title={`回程 Agent → Probe 丢包 ${fmtPct(down)}`}>
|
||||
<span>回程</span>
|
||||
<span className={`line-mini-badge ${levelOf(down)}`}>{downText}</span>
|
||||
</div>
|
||||
<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'])}`}>
|
||||
<div className="line-mini-tcp">
|
||||
<div className="line-mini-tcp-head" title={`TCP RTT p50 ${fmtMs(m['tcp.rtt.p50'])}`}>
|
||||
<span>TCP</span><strong className="num">{fmtMs(m['tcp.rtt.p50'])}</strong>
|
||||
<span className={`line-mini-badge ${retransTone}`}>{retransText}</span>
|
||||
</div>
|
||||
<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}>
|
||||
|
||||
+58
-16
@@ -116,6 +116,29 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.line-loading-state,
|
||||
.line-empty-state {
|
||||
display: flex;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--glass-ring);
|
||||
border-radius: var(--r-card);
|
||||
background: color-mix(in srgb, var(--glass-bg) 75%, transparent);
|
||||
color: var(--app-ink-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.line-loading-state .loading-bar {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.line-empty-state {
|
||||
color: var(--app-ink-faint);
|
||||
}
|
||||
|
||||
.line-page .faint,
|
||||
.line-page .chart-hint,
|
||||
.line-page .chart-section h2,
|
||||
@@ -1359,15 +1382,15 @@
|
||||
}
|
||||
|
||||
.line-mini-quic-head > span,
|
||||
.line-mini-direction-row > span:first-child,
|
||||
.line-mini-tcp-row > span:first-child {
|
||||
.line-mini-direction-head > span:first-child,
|
||||
.line-mini-tcp-head > span:first-child {
|
||||
color: var(--app-ink-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.line-mini-quic-head > strong,
|
||||
.line-mini-tcp-row > strong {
|
||||
.line-mini-tcp-head > strong {
|
||||
color: var(--app-ink);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@@ -1381,32 +1404,40 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.line-mini-direction-row {
|
||||
.line-mini-direction,
|
||||
.line-mini-tcp {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) 66px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.line-mini-direction-row > span:first-child,
|
||||
.line-mini-tcp-row > span:first-child {
|
||||
.line-mini-direction-head,
|
||||
.line-mini-tcp-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.line-mini-direction-head > span:first-child,
|
||||
.line-mini-tcp-head > span:first-child {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.line-mini-tcp-row {
|
||||
display: grid;
|
||||
grid-template-columns: 34px 46px minmax(0, 1fr) max-content;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
.line-mini-direction-head .line-mini-badge,
|
||||
.line-mini-tcp-head .line-mini-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.line-mini-tcp {
|
||||
margin-top: 2px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid var(--app-outline-soft);
|
||||
}
|
||||
|
||||
.line-mini-tcp-row > strong {
|
||||
.line-mini-tcp-head > strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1693,6 +1724,7 @@
|
||||
.agent-head .flag {
|
||||
flex: none;
|
||||
width: 24px;
|
||||
max-width: 24px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
object-fit: cover;
|
||||
@@ -1801,6 +1833,16 @@
|
||||
background: color-mix(in srgb, var(--app-ink) 7%, transparent);
|
||||
}
|
||||
|
||||
.agent-card .line-link.disconnected,
|
||||
.agent-card .line-link.disconnected:hover {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.line-range.disconnected-label {
|
||||
color: var(--app-ink-faint);
|
||||
}
|
||||
|
||||
|
||||
/* 线路详情、对比等页面的卡片和指标块:同样的亚克力,一层压一层 */
|
||||
.card:not(.agent-card),
|
||||
|
||||
@@ -286,29 +286,16 @@ export function Line() {
|
||||
|
||||
<PageError error={line.error ?? series.error} />
|
||||
|
||||
{!s && (loadingNow || !line.error) && !series.error ? (
|
||||
// 还没有数据:先摆出占位,每块上面有进度条
|
||||
<div className="stack" style={{ marginTop: 24 }}>
|
||||
<div className="tiles">
|
||||
{Array.from({ length: 12 }, (_, i) => (
|
||||
<div className="skeleton has-loading" key={i}>
|
||||
{!s && loadingNow && !line.error && !series.error ? (
|
||||
<div className="line-loading-state has-loading" role="status" aria-live="polite">
|
||||
<LoadingBar active />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="skeleton chart has-loading">
|
||||
<LoadingBar active />
|
||||
</div>
|
||||
<div className="grid cols-2">
|
||||
{[0, 1].map((i) => (
|
||||
<div className="skeleton chart has-loading" key={i}>
|
||||
<LoadingBar active />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Spinner />
|
||||
<span>正在加载线路数据…</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!s && !loadingNow && !series.error && line.data ? <div className="line-empty-state">所选时间段暂无测量数据。</div> : null}
|
||||
|
||||
{s ? (
|
||||
<div className="stack line-metrics" style={{ marginTop: 16 }}>
|
||||
<section className="line-metric-surface">
|
||||
|
||||
@@ -141,8 +141,18 @@ function NodeCard({ node, labels, infos }: { node: HostItem; labels: string[]; i
|
||||
function LineCard({ l, range, infos }: { l: OverviewLineDto; range: OverviewRange; infos: Map<string, NodeInfoDto> }) {
|
||||
const probeInfo = infos.get(l.probe_id)
|
||||
const agentInfo = infos.get(l.agent_id)
|
||||
const rangeLabel = OVERVIEW_RANGES.find((r) => r.value === range)?.label ?? range
|
||||
const connectionIssue = !l.online
|
||||
? 'Agent 离线'
|
||||
: l.quic_health?.state === 'retry_wait'
|
||||
? 'QUIC 拨号失败'
|
||||
: !l.probe_connected
|
||||
? '测量通道未建立'
|
||||
: l.quic_health && !l.quic_health.connected
|
||||
? 'QUIC 正在连接'
|
||||
: null
|
||||
return (
|
||||
<Link to={`/line/${encodeURIComponent(l.id)}`} className="line-link line-card">
|
||||
<Link to={`/line/${encodeURIComponent(l.id)}`} className={`line-link line-card${connectionIssue ? ' disconnected' : ''}`}>
|
||||
<div className="line-top">
|
||||
<span className="line-name" title={[l.probe_name, probeInfo?.tag || probeInfo?.version].filter(Boolean).join(' · ')}>
|
||||
{probeInfo?.country ? <Flag code={probeInfo.country} /> : null}
|
||||
@@ -152,13 +162,12 @@ function LineCard({ l, range, infos }: { l: OverviewLineDto; range: OverviewRang
|
||||
{agentInfo?.country ? <Flag code={agentInfo.country} /> : null}
|
||||
<span className="name">{l.name}</span>
|
||||
</span>
|
||||
{!l.probe_connected && l.online ? (
|
||||
<span title="Agent 在线,但这个 Probe 没能和它建立测量会话(控制通道连不上或握手超时)。机器本身可能是通的、ping 也能通,这里说的是测量用的 TCP 控制通道和 QUIC Datagram 数据通道,线路或运营商对它们的处理可能和 ping 不一样。">
|
||||
<Chip tone="warn">测量通道未建立</Chip>
|
||||
<span
|
||||
className={connectionIssue ? 'line-range disconnected-label' : 'faint line-range'}
|
||||
title={l.quic_health?.state === 'retry_wait' ? `Probe 正在等待退避重试${l.quic_health.retry_in_ms ? `(约 ${Math.ceil(l.quic_health.retry_in_ms / 1000)} 秒)` : ''}。` : !l.probe_connected && l.online ? 'Agent 在线,但 Probe 尚未建立测量会话。' : undefined}
|
||||
>
|
||||
{connectionIssue ? `${connectionIssue} · ${rangeLabel}` : rangeLabel}
|
||||
</span>
|
||||
) : (
|
||||
<span className="faint line-range">{OVERVIEW_RANGES.find((r) => r.value === range)?.label ?? range}</span>
|
||||
)}
|
||||
</div>
|
||||
<LineStrips lineId={l.id} range={range} m={l.summary.metrics} />
|
||||
</Link>
|
||||
@@ -254,7 +263,7 @@ export function Overview() {
|
||||
<IpVersionPicker value={ipVersion} onChange={(v) => { const next = new URLSearchParams(params); next.set('ip_version', v); setCarrier(''); setParams(next, { replace: true }) }} />
|
||||
{groups.length > 1 ? <Segmented<'' | CarrierKey> options={[{ value: '', label: '全部' }, ...groups.map((g) => ({ value: g.key, label: g.name }))]} value={carrier} onChange={setCarrier} /> : null}
|
||||
<span className="right row">
|
||||
<Segmented<OverviewRange> options={OVERVIEW_RANGES.map((r) => ({ value: r.value, label: `近 ${r.label}` }))} value={range} onChange={setRange} />
|
||||
<Segmented<OverviewRange> options={OVERVIEW_RANGES.map((r) => ({ value: r.value, label: r.label }))} value={range} onChange={setRange} />
|
||||
<Segmented
|
||||
options={[
|
||||
{ value: 'name' as const, label: '名称' },
|
||||
|
||||
Reference in new issue
Block a user