fix(uptime): показывать реальные наблюдения и пропуски RTT на дашборде

This commit is contained in:
Denozordec committed 2026-10-03 23:42:45 +07:00
1 parent 8f65d1620d
commit 9d3129372b
18 files changed
+169 -203

No files matched your search

+25 -127
View File
@@ -1,129 +1,27 @@
"use client"
const PALETTE = [
"var(--chart-line-1)",
"var(--chart-line-2)",
"var(--chart-line-3)",
"var(--chart-line-4)",
"var(--chart-1)",
"var(--chart-2)",
"var(--chart-3)",
"var(--chart-4)",
]
/** Легенда для демо-данных (ключи `msk`, `spb`, …). */
const LEGACY_LABELS: Record<string, string> = {
msk: "MSK",
spb: "SPB",
fra: "FRA",
ams: "AMS",
}
interface LatencyChartProps {
series: Record<string, number[]>
/** Подписи линий (ключ → короткое имя). Если нет — берётся legacy или сам ключ. */
labels?: Record<string, string>
}
export function LatencyChart({ series, labels: labelsProp }: LatencyChartProps) {
const W = 800
const H = 220
const pad = { l: 44, r: 12, t: 12, b: 28 }
const iw = W - pad.l - pad.r
const ih = H - pad.t - pad.b
const entries = Object.entries(series).filter(([, arr]) => arr.length > 0)
const pointCount = entries.length ? Math.max(...entries.map(([, a]) => a.length), 2) : 60
const allVals = entries.flatMap(([, arr]) => arr)
const maxVal = Math.ceil(Math.max(1, ...allVals) / 20) * 20 + 10
const xAt = (i: number, n: number) => pad.l + (i / Math.max(n - 1, 1)) * iw
const yAt = (v: number) => pad.t + (1 - v / maxVal) * ih
const gridLines = [0, 0.25, 0.5, 0.75, 1]
const tickIndices = [
0,
Math.floor((pointCount - 1) * 0.25),
Math.floor((pointCount - 1) * 0.5),
Math.floor((pointCount - 1) * 0.75),
pointCount - 1,
]
const labelForIndex = (i: number) => {
const spanMin = 60
const m = Math.round(spanMin * (1 - i / Math.max(pointCount - 1, 1)))
return `-${m}м`
}
return (
<div>
<svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: 220, display: "block" }}>
{gridLines.map((p, i) => (
<g key={i}>
<line
x1={pad.l}
x2={W - pad.r}
y1={pad.t + ih * p}
y2={pad.t + ih * p}
style={{ stroke: "var(--border)" }}
strokeDasharray={p === 1 ? "0" : "2 4"}
/>
<text
x={pad.l - 8}
y={pad.t + ih * p + 4}
textAnchor="end"
fontSize="10"
style={{ fill: "var(--muted-foreground)" }}
fontFamily="var(--font-mono, monospace)"
>
{Math.round(maxVal * (1 - p))}мс
</text>
</g>
))}
{tickIndices.map((i) => (
<text
key={i}
x={xAt(i, pointCount)}
y={H - 6}
textAnchor="middle"
fontSize="10"
style={{ fill: "var(--muted-foreground)" }}
fontFamily="var(--font-mono, monospace)"
>
{labelForIndex(i)}
</text>
))}
{entries.map(([key, arr], idx) => {
const n = arr.length
const pts = arr.map((v, i) => `${xAt(i, n).toFixed(1)},${yAt(v).toFixed(1)}`).join(" ")
const color = PALETTE[idx % PALETTE.length]
return (
<polyline
key={key}
points={pts}
fill="none"
style={{ stroke: color }}
strokeWidth="1.6"
strokeLinejoin="round"
/>
)
})}
</svg>
<div className="flex flex-wrap gap-x-4 gap-y-2 pt-2 pl-11">
{entries.map(([key], idx) => {
const color = PALETTE[idx % PALETTE.length]
const leg =
labelsProp?.[key] ?? LEGACY_LABELS[key] ?? key.replace(/^src-/, "")
return (
<div key={key} className="flex items-center gap-1.5 text-xs">
<span className="w-3 h-0.5 rounded" style={{ background: color }} />
<span className="font-mono text-foreground">{leg}</span>
</div>
)
})}
</div>
</div>
)
import {CartesianGrid,Line,LineChart,XAxis,YAxis} from "recharts"
import {ChartContainer,ChartTooltip,ChartTooltipContent,type ChartConfig} from "@/components/ui/chart"
import {formatRangeAgo} from "@/lib/fmt-rate"
const PALETTE=["var(--chart-line-1)","var(--chart-line-2)","var(--chart-line-3)","var(--chart-line-4)","var(--chart-1)","var(--chart-2)","var(--chart-3)","var(--chart-4)"]
const LEGACY_LABELS:Record<string,string>={msk:"MSK",spb:"SPB",fra:"FRA",ams:"AMS"}
interface LatencyChartProps {series:Record<string,(number|null)[]>;labels?:Record<string,string>}
/** Reuse the installed ChartContainer / multi-line composition of ReUI chart-23. */
export function LatencyChart({series,labels:labelsProp}:LatencyChartProps){
const entries=Object.entries(series).filter(([,values])=>values.some(v=>typeof v==="number"&&Number.isFinite(v)))
const count=Math.max(1,...entries.map(([,values])=>values.length))
const data=Array.from({length:count},(_,i)=>({time:formatRangeAgo(Math.round((1-i/Math.max(count-1,1))*60)),...Object.fromEntries(entries.map(([key,values])=>[key,typeof values[i]==="number"&&Number.isFinite(values[i])?values[i]:null]))}))
const label=(key:string)=>labelsProp?.[key]??LEGACY_LABELS[key]??key.replace(/^src-/,"")
const config=Object.fromEntries(entries.map(([key],index)=>[key,{label:label(key),color:PALETTE[index%PALETTE.length]}])) satisfies ChartConfig
const dot=(key:string)=>function IsolatedDot({cx,cy,index}:{cx?:number;cy?:number;index?:number}){const values=series[key];if(index==null||cx==null||cy==null||values[index]==null||values[index-1]!=null||values[index+1]!=null)return <g/>;return <circle cx={cx} cy={cy} r={3} fill={config[key].color} data-latency-isolated="true"/>}
if(!entries.length)return <p role="status" className="py-10 text-center text-sm text-muted-foreground">Нет измеренной истории RTT</p>
return <div role="region" aria-label="История RTT по источникам" className="flex flex-col gap-3">
<ChartContainer config={config} className="aspect-auto h-[220px] w-full"><LineChart data={data} margin={{top:8,right:8,left:0,bottom:0}}>
<CartesianGrid vertical={false} strokeDasharray="4 8" stroke="var(--border)"/>
<XAxis dataKey="time" axisLine={false} tickLine={false} tick={{fontSize:11}} tickMargin={8} minTickGap={16}/>
<YAxis axisLine={false} tickLine={false} tick={{fontSize:11}} width={52} tickFormatter={(v:number)=>String(v)+" мс"}/>
<ChartTooltip content={<ChartTooltipContent formatter={(value,name)=><div className="flex min-w-0 items-center gap-2 text-xs"><span>{config[String(name)]?.label??name}</span><span className="font-mono">{typeof value==="number"?String(value)+" мс":"—"}</span></div>}/>}/>
{entries.map(([key])=><Line key={key} dataKey={key} type="linear" stroke={config[key].color} strokeWidth={1.6} connectNulls={false} dot={dot(key)} isAnimationActive={false}/>)}
</LineChart></ChartContainer>
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2">{entries.map(([key])=><span key={key} data-latency-source={key} className="flex items-center gap-1.5 text-xs"><span aria-hidden className="h-0.5 w-3 rounded" style={{background:config[key].color}}/><span className="text-foreground">{label(key)}</span></span>)}</div>
</div>
}