fix(ui): обновлять детали и пояснять модель карты сети
This commit is contained in:
1 parent
6cb27b2aab
commit
0cdb43a29d
6 files changed
+133
-63
No files matched your search
@@ -436,7 +436,7 @@ function pingColor(ms: number | null) {
|
|||||||
return "#fb923c"
|
return "#fb923c"
|
||||||
}
|
}
|
||||||
function latencyColor(ms: number | null) {
|
function latencyColor(ms: number | null) {
|
||||||
if (ms == null) return "#f87171"
|
if (ms == null) return "var(--muted-foreground)"
|
||||||
if (ms > 60) return "#fbbf24"
|
if (ms > 60) return "#fbbf24"
|
||||||
return "#4ade80"
|
return "#4ade80"
|
||||||
}
|
}
|
||||||
@@ -506,7 +506,7 @@ function PingBadge({ mx, my, ping, dl, ul, color }: {
|
|||||||
</text>
|
</text>
|
||||||
{hasSpeed && (
|
{hasSpeed && (
|
||||||
<text textAnchor="middle" y="10" fontSize="7" fill="#64748b" fontFamily="ui-monospace,monospace">
|
<text textAnchor="middle" y="10" fontSize="7" fill="#64748b" fontFamily="ui-monospace,monospace">
|
||||||
{`↓${dl} ↑${ul}`}
|
{`↓${dl??"—"} ↑${ul??"—"}`}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
@@ -524,6 +524,7 @@ function GreEdgeMetricBadge({
|
|||||||
rttFromMonitor,
|
rttFromMonitor,
|
||||||
throughputFromMonitor,
|
throughputFromMonitor,
|
||||||
outerSummary,
|
outerSummary,
|
||||||
|
probe,
|
||||||
}: {
|
}: {
|
||||||
mx: number
|
mx: number
|
||||||
my: number
|
my: number
|
||||||
@@ -536,11 +537,9 @@ function GreEdgeMetricBadge({
|
|||||||
throughputFromMonitor?: boolean
|
throughputFromMonitor?: boolean
|
||||||
/** Внешние IP GRE и WAN — только в подсказке при наведении */
|
/** Внешние IP GRE и WAN — только в подсказке при наведении */
|
||||||
outerSummary?: string
|
outerSummary?: string
|
||||||
|
probe?: GreSpeedProbeSnapshot
|
||||||
}) {
|
}) {
|
||||||
const hasSpeed =
|
const hasSpeed = dl != null || ul != null
|
||||||
dl != null &&
|
|
||||||
ul != null &&
|
|
||||||
(throughputFromMonitor || dl > 0 || ul > 0)
|
|
||||||
const bw = 62
|
const bw = 62
|
||||||
const bh = 24 + (hasSpeed ? 14 : 0)
|
const bh = 24 + (hasSpeed ? 14 : 0)
|
||||||
const v1 = pingMs == null ? "—" : `${pingMs} мс`
|
const v1 = pingMs == null ? "—" : `${pingMs} мс`
|
||||||
@@ -548,8 +547,9 @@ function GreEdgeMetricBadge({
|
|||||||
(outerSummary ? `${outerSummary}. ` : "") +
|
(outerSummary ? `${outerSummary}. ` : "") +
|
||||||
(rttFromMonitor
|
(rttFromMonitor
|
||||||
? "RTT — ping/BT из «Мониторинг → скорость». "
|
? "RTT — ping/BT из «Мониторинг → скорость». "
|
||||||
: "RTT — модель по данным туннеля. ") +
|
: pingMs==null?"RTT неизвестен. ":"RTT — демонстрационная модель. ") +
|
||||||
(throughputFromMonitor ? "Строка ниже — TX/RX с пробы. " : "") +
|
(throughputFromMonitor ? "Строка ниже — TX/RX с пробы. " : "") +
|
||||||
|
(probe?"Проба "+probe.id+"; время Ping: "+(probe.lastPingAt?new Date(probe.lastPingAt).toLocaleString("ru-RU"):"неизвестно")+"; время TX/RX неизвестно. ":"") +
|
||||||
"Клик — панель справа."
|
"Клик — панель справа."
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
@@ -679,7 +679,9 @@ function ServerNode({ n, isSel, isVis, isDragged, hideCatalogLatency, onClick, o
|
|||||||
const ts = TYPE_STYLE[n.type]
|
const ts = TYPE_STYLE[n.type]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g transform={`translate(${n.x},${n.y})`}
|
<g role="button" aria-label={"Устройство "+n.name} aria-pressed={isSel} tabIndex={isVis?0:-1}
|
||||||
|
onKeyDown={e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();e.stopPropagation();onClick()}}}
|
||||||
|
transform={`translate(${n.x},${n.y})`}
|
||||||
style={{ cursor: isDragged ? "grabbing" : "grab", transition: isDragged ? "none" : "opacity 0.25s" }}
|
style={{ cursor: isDragged ? "grabbing" : "grab", transition: isDragged ? "none" : "opacity 0.25s" }}
|
||||||
opacity={isVis ? 1 : 0.07}
|
opacity={isVis ? 1 : 0.07}
|
||||||
onMouseDown={e => { e.stopPropagation(); onMouseDown(e) }}
|
onMouseDown={e => { e.stopPropagation(); onMouseDown(e) }}
|
||||||
@@ -691,8 +693,6 @@ function ServerNode({ n, isSel, isVis, isDragged, hideCatalogLatency, onClick, o
|
|||||||
>
|
>
|
||||||
{n.status === "online" && isVis && (
|
{n.status === "online" && isVis && (
|
||||||
<circle r={ts.r + 14} fill={ss.glow} opacity="0.7">
|
<circle r={ts.r + 14} fill={ss.glow} opacity="0.7">
|
||||||
<animate attributeName="r" values={`${ts.r+10};${ts.r+18};${ts.r+10}`} dur="3s" repeatCount="indefinite" />
|
|
||||||
<animate attributeName="opacity" values="0.8;0.15;0.8" dur="3s" repeatCount="indefinite" />
|
|
||||||
</circle>
|
</circle>
|
||||||
)}
|
)}
|
||||||
{n.status === "degraded" && isVis && (
|
{n.status === "degraded" && isVis && (
|
||||||
@@ -753,13 +753,13 @@ function ServerNode({ n, isSel, isVis, isDragged, hideCatalogLatency, onClick, o
|
|||||||
)}
|
)}
|
||||||
{!hideCatalogLatency && (
|
{!hideCatalogLatency && (
|
||||||
<text textAnchor="middle" y="12" fontSize="8.5" fill={latencyColor(n.latency)} fontFamily="ui-monospace,monospace">
|
<text textAnchor="middle" y="12" fontSize="8.5" fill={latencyColor(n.latency)} fontFamily="ui-monospace,monospace">
|
||||||
{n.latency == null ? "офлайн" : `${n.latency} мс`}
|
{n.latency == null ? "—" : `${n.latency} мс`}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
{n.type === "home-router" && (
|
{(n.name && (
|
||||||
<text textAnchor="middle" y={hideCatalogLatency ? 15 : ts.r + 18} fontSize="8.5" fill="#94a3b8"
|
<text textAnchor="middle" y={hideCatalogLatency ? 15 : ts.r + 18} fontSize="8.5" fill="#94a3b8"
|
||||||
fontFamily="ui-monospace,monospace">{shortName(n.name)}</text>
|
fontFamily="ui-monospace,monospace">{shortName(n.name)}</text>
|
||||||
)}
|
))}
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1147,6 +1147,7 @@ function NetworkMapContent() {
|
|||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [mapServers, setMapServers] = useState<Server[]>([])
|
const [mapServers, setMapServers] = useState<Server[]>([])
|
||||||
|
const [sessionCounts,setSessionCounts]=useState<Record<string,number|null>>({})
|
||||||
const [mapGreTunnels, setMapGreTunnels] = useState<GreTunnel[]>([])
|
const [mapGreTunnels, setMapGreTunnels] = useState<GreTunnel[]>([])
|
||||||
const [speedProbes, setSpeedProbes] = useState<GreSpeedProbeSnapshot[]>([])
|
const [speedProbes, setSpeedProbes] = useState<GreSpeedProbeSnapshot[]>([])
|
||||||
const [mapHops, setMapHops] = useState<FlowMapHop[]>([])
|
const [mapHops, setMapHops] = useState<FlowMapHop[]>([])
|
||||||
@@ -1206,6 +1207,7 @@ function NetworkMapContent() {
|
|||||||
const mappedTunnels=snapshot.tunnels.map(apiGreToGreTunnel)
|
const mappedTunnels=snapshot.tunnels.map(apiGreToGreTunnel)
|
||||||
if(!current())return
|
if(!current())return
|
||||||
setMapServers(rows.map(mapBackendRowToServer));setMapGreTunnels(mappedTunnels)
|
setMapServers(rows.map(mapBackendRowToServer));setMapGreTunnels(mappedTunnels)
|
||||||
|
setSessionCounts(Object.fromEntries(rows.map(row=>[String(row.id),row.sessions??null])))
|
||||||
setSpeedProbes(snapshot.probes.map(probe=>({...probe,srcInterface:probe.srcInterface||undefined,dstInterface:probe.dstInterface||undefined})))
|
setSpeedProbes(snapshot.probes.map(probe=>({...probe,srcInterface:probe.srcInterface||undefined,dstInterface:probe.dstInterface||undefined})))
|
||||||
setHasSnapshot(true);setReadAt(new Date().toISOString());setDataError(null)
|
setHasSnapshot(true);setReadAt(new Date().toISOString());setDataError(null)
|
||||||
const hostSet = new Set<string>(collectGreEndpointHostnames(mappedTunnels))
|
const hostSet = new Set<string>(collectGreEndpointHostnames(mappedTunnels))
|
||||||
@@ -1283,7 +1285,7 @@ function NetworkMapContent() {
|
|||||||
() => computeNetworkMapLayout(mapServers, mapGreTunnels, greResolvedMap),
|
() => computeNetworkMapLayout(mapServers, mapGreTunnels, greResolvedMap),
|
||||||
[mapServers, mapGreTunnels, greResolvedMap],
|
[mapServers, mapGreTunnels, greResolvedMap],
|
||||||
)
|
)
|
||||||
const wanJhEdges = useMemo(() => buildWanJhEdges(mapServers), [mapServers])
|
const wanJhEdges = useMemo(() => buildWanJhEdges(mapServers).map(edge=>({...edge,active:!useLiveData&&edge.active})), [mapServers,useLiveData])
|
||||||
const srvResMap = useMemo(() => buildServerResourceMap(mapServers, !useLiveData), [mapServers,useLiveData])
|
const srvResMap = useMemo(() => buildServerResourceMap(mapServers, !useLiveData), [mapServers,useLiveData])
|
||||||
const homeRouters = useMemo(
|
const homeRouters = useMemo(
|
||||||
() => mapServers.filter((s) => s.type === "home-router"),
|
() => mapServers.filter((s) => s.type === "home-router"),
|
||||||
@@ -1296,7 +1298,8 @@ function NetworkMapContent() {
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
|
||||||
// ── Interaction ─────────────────────────────────────────────────────────────
|
// ── Interaction ─────────────────────────────────────────────────────────────
|
||||||
const [selected, setSelected] = useState<Server | null>(null)
|
const [selectedStored, setSelected] = useState<Server | null>(null)
|
||||||
|
const selected=selectedStored?mapServers.find(server=>server.id===selectedStored.id)??null:null
|
||||||
const [selectedService, setSelectedService] = useState<FlowMapService | null>(null)
|
const [selectedService, setSelectedService] = useState<FlowMapService | null>(null)
|
||||||
/** Раскрытая страна (режим «Страны»): справа столбец её сервисов. Не персистится. */
|
/** Раскрытая страна (режим «Страны»): справа столбец её сервисов. Не персистится. */
|
||||||
const [expandedCountryId, setExpandedCountryId] = useState<string | null>(null)
|
const [expandedCountryId, setExpandedCountryId] = useState<string | null>(null)
|
||||||
@@ -1350,9 +1353,7 @@ function NetworkMapContent() {
|
|||||||
if (sessionStorage.getItem(k) !== String(NETWORK_MAP_LAYOUT_REVISION)) {
|
if (sessionStorage.getItem(k) !== String(NETWORK_MAP_LAYOUT_REVISION)) {
|
||||||
// Важно: без явного сброса старые drag-координаты остаются в state
|
// Важно: без явного сброса старые drag-координаты остаются в state
|
||||||
// и перекрывают новый авто-лейаут на live-данных.
|
// и перекрывают новый авто-лейаут на live-данных.
|
||||||
setNodePositions({})
|
queueMicrotask(()=>{setNodePositions({});setSatPositions({});setServicePositions({})})
|
||||||
setSatPositions({})
|
|
||||||
setServicePositions({})
|
|
||||||
sessionStorage.setItem(k, String(NETWORK_MAP_LAYOUT_REVISION))
|
sessionStorage.setItem(k, String(NETWORK_MAP_LAYOUT_REVISION))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1363,7 +1364,7 @@ function NetworkMapContent() {
|
|||||||
// ── Context menu ────────────────────────────────────────────────────────────
|
// ── Context menu ────────────────────────────────────────────────────────────
|
||||||
const [ctxMenu, setCtxMenu] = useState<CtxMenu | null>(null)
|
const [ctxMenu, setCtxMenu] = useState<CtxMenu | null>(null)
|
||||||
|
|
||||||
const [selectedGreEdge, setSelectedGreEdge] = useState<GreMapEdge | null>(null)
|
const [selectedGreStored, setSelectedGreEdge] = useState<GreMapEdge | null>(null)
|
||||||
|
|
||||||
// ── Toggles ─────────────────────────────────────────────────────────────────
|
// ── Toggles ─────────────────────────────────────────────────────────────────
|
||||||
const [filter, setFilter] = useState<FilterKey>("all")
|
const [filter, setFilter] = useState<FilterKey>("all")
|
||||||
@@ -1524,6 +1525,8 @@ function NetworkMapContent() {
|
|||||||
[greEdges, speedProbes, greResolvedMap],
|
[greEdges, speedProbes, greResolvedMap],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const selectedGreEdge=selectedGreStored?greEdges.find(edge=>greEdgeKey(edge)===greEdgeKey(selectedGreStored))??null:null
|
||||||
|
|
||||||
/** WAN→JH: не показывать GreEdgeMetricBadge со speed, если те же метрики уже на GRE (один бейдж на сегмент). */
|
/** WAN→JH: не показывать GreEdgeMetricBadge со speed, если те же метрики уже на GRE (один бейдж на сегмент). */
|
||||||
const suppressWanJhSpeedBadgeDuplicate = useMemo(() => {
|
const suppressWanJhSpeedBadgeDuplicate = useMemo(() => {
|
||||||
const s = new Set<string>()
|
const s = new Set<string>()
|
||||||
@@ -1697,8 +1700,7 @@ function NetworkMapContent() {
|
|||||||
|
|
||||||
const zoomRef = useRef(zoom)
|
const zoomRef = useRef(zoom)
|
||||||
const panRef = useRef(pan)
|
const panRef = useRef(pan)
|
||||||
zoomRef.current = zoom
|
useEffect(()=>{zoomRef.current=zoom;panRef.current=pan},[zoom,pan])
|
||||||
panRef.current = pan
|
|
||||||
|
|
||||||
// ── SVG export ───────────────────────────────────────────────────────────
|
// ── SVG export ───────────────────────────────────────────────────────────
|
||||||
function exportSvg() {
|
function exportSvg() {
|
||||||
@@ -1746,6 +1748,27 @@ function NetworkMapContent() {
|
|||||||
return () => svg.removeEventListener("wheel", handler)
|
return () => svg.removeEventListener("wheel", handler)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// ── Zoom helpers ──────────────────────────────────────────────────────────
|
||||||
|
const applyZoomCenter=useCallback((factor: number) => {
|
||||||
|
const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * factor))
|
||||||
|
setPan(p => ({
|
||||||
|
x: p.x + (W / zoom - W / newZoom) / 2,
|
||||||
|
y: p.y + (H / zoom - H / newZoom) / 2,
|
||||||
|
}))
|
||||||
|
setZoom(newZoom)
|
||||||
|
},[zoom])
|
||||||
|
|
||||||
|
const fitView=useCallback(() => {
|
||||||
|
setPan({ x: 0, y: 0 })
|
||||||
|
setZoom(1)
|
||||||
|
},[])
|
||||||
|
|
||||||
|
function focusNode(x: number, y: number) {
|
||||||
|
const newZoom = Math.min(ZOOM_MAX, Math.max(zoom * 1.8, 2))
|
||||||
|
setPan({ x: x - W / (2 * newZoom), y: y - H / (2 * newZoom) })
|
||||||
|
setZoom(newZoom)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Keyboard shortcuts ────────────────────────────────────────────────────
|
// ── Keyboard shortcuts ────────────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onKey(e: KeyboardEvent) {
|
function onKey(e: KeyboardEvent) {
|
||||||
@@ -1766,28 +1789,7 @@ function NetworkMapContent() {
|
|||||||
}
|
}
|
||||||
window.addEventListener("keydown", onKey)
|
window.addEventListener("keydown", onKey)
|
||||||
return () => window.removeEventListener("keydown", onKey)
|
return () => window.removeEventListener("keydown", onKey)
|
||||||
}, [zoom, pan]) // eslint-disable-line react-hooks/exhaustive-deps -- applyZoomCenter/fitView ниже; handler обновляется через [zoom, pan]
|
}, [applyZoomCenter,fitView])
|
||||||
|
|
||||||
// ── Zoom helpers ──────────────────────────────────────────────────────────
|
|
||||||
function applyZoomCenter(factor: number) {
|
|
||||||
const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * factor))
|
|
||||||
setPan(p => ({
|
|
||||||
x: p.x + (W / zoom - W / newZoom) / 2,
|
|
||||||
y: p.y + (H / zoom - H / newZoom) / 2,
|
|
||||||
}))
|
|
||||||
setZoom(newZoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fitView() {
|
|
||||||
setPan({ x: 0, y: 0 })
|
|
||||||
setZoom(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusNode(x: number, y: number) {
|
|
||||||
const newZoom = Math.min(ZOOM_MAX, Math.max(zoom * 1.8, 2))
|
|
||||||
setPan({ x: x - W / (2 * newZoom), y: y - H / (2 * newZoom) })
|
|
||||||
setZoom(newZoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Drag ─────────────────────────────────────────────────────────────────
|
// ── Drag ─────────────────────────────────────────────────────────────────
|
||||||
function onSvgMouseDown(e: React.MouseEvent<SVGSVGElement>) {
|
function onSvgMouseDown(e: React.MouseEvent<SVGSVGElement>) {
|
||||||
@@ -1966,7 +1968,7 @@ function NetworkMapContent() {
|
|||||||
setHoveredId(null)
|
setHoveredId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedTunnels = selected
|
const connectedTunnels = useMemo(()=>selected
|
||||||
? mapGreTunnels.filter((t) => {
|
? mapGreTunnels.filter((t) => {
|
||||||
if (t.serverId === selected.id) return true
|
if (t.serverId === selected.id) return true
|
||||||
const peer = findServerByGreRemote(mapServers, t.remoteAddress, greResolvedMap)
|
const peer = findServerByGreRemote(mapServers, t.remoteAddress, greResolvedMap)
|
||||||
@@ -1983,7 +1985,7 @@ function NetworkMapContent() {
|
|||||||
selectedWanIps.includes(local)
|
selectedWanIps.includes(local)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
: []
|
: [],[selected,mapGreTunnels,mapServers,greResolvedMap])
|
||||||
const grePrimaryByGroupKey = useMemo(() => {
|
const grePrimaryByGroupKey = useMemo(() => {
|
||||||
if (!selected) return new Map<string, string>()
|
if (!selected) return new Map<string, string>()
|
||||||
const group = new Map<string, GreTunnel[]>()
|
const group = new Map<string, GreTunnel[]>()
|
||||||
@@ -2182,6 +2184,7 @@ function NetworkMapContent() {
|
|||||||
{useLiveData&&flowLoading&&(showNetflow||showServices)&&<span role="status">Обновление NetFlow…</span>}
|
{useLiveData&&flowLoading&&(showNetflow||showServices)&&<span role="status">Обновление NetFlow…</span>}
|
||||||
{useLiveData&&flowError&&(showNetflow||showServices)&&<span role="alert" className="text-destructive break-all">NetFlow: {flowError}. {flowReadAt?"Сохранён предыдущий снимок NetFlow; актуальность не подтверждена.":"Первоначальная загрузка NetFlow не подтверждена."}</span>}
|
{useLiveData&&flowError&&(showNetflow||showServices)&&<span role="alert" className="text-destructive break-all">NetFlow: {flowError}. {flowReadAt?"Сохранён предыдущий снимок NetFlow; актуальность не подтверждена.":"Первоначальная загрузка NetFlow не подтверждена."}</span>}
|
||||||
{useLiveData&&dnsError&&<span role="alert" className="text-destructive break-all">DNS: {dnsError}. Связи по неизвестному адресу не подтверждены.</span>}
|
{useLiveData&&dnsError&&<span role="alert" className="text-destructive break-all">DNS: {dnsError}. Связи по неизвестному адресу не подтверждены.</span>}
|
||||||
|
<span>Пунктир WAN→JH — модель связности; маршрут не подтверждён.</span>
|
||||||
{dataLoading && <span role="status">Обновление снимка карты…</span>}
|
{dataLoading && <span role="status">Обновление снимка карты…</span>}
|
||||||
{dataError && (
|
{dataError && (
|
||||||
<span role="alert" className="text-[10px] text-destructive break-all">
|
<span role="alert" className="text-[10px] text-destructive break-all">
|
||||||
@@ -2327,6 +2330,7 @@ function NetworkMapContent() {
|
|||||||
dl={merged.dlMbps}
|
dl={merged.dlMbps}
|
||||||
ul={merged.ulMbps}
|
ul={merged.ulMbps}
|
||||||
onOpen={openGreDetail}
|
onOpen={openGreDetail}
|
||||||
|
probe={spGre}
|
||||||
rttFromMonitor={rttFromMonitor}
|
rttFromMonitor={rttFromMonitor}
|
||||||
throughputFromMonitor={throughputFromMonitor}
|
throughputFromMonitor={throughputFromMonitor}
|
||||||
outerSummary={greOuterSummaryLine(e.tunnel, fromN, toN, greResolvedMap)}
|
outerSummary={greOuterSummaryLine(e.tunnel, fromN, toN, greResolvedMap)}
|
||||||
@@ -2395,7 +2399,7 @@ function NetworkMapContent() {
|
|||||||
const kj = wanJhEdgeMapKey(edge)
|
const kj = wanJhEdgeMapKey(edge)
|
||||||
const sp = speedProbeByWanJhKey.get(kj)
|
const sp = speedProbeByWanJhKey.get(kj)
|
||||||
const merged = mergeGreMetricsWithSpeedProbe(sp, {
|
const merged = mergeGreMetricsWithSpeedProbe(sp, {
|
||||||
pingMs: edge.pingMs,
|
pingMs: useLiveData?null:edge.pingMs,
|
||||||
dlMbps: null,
|
dlMbps: null,
|
||||||
ulMbps: null,
|
ulMbps: null,
|
||||||
})
|
})
|
||||||
@@ -2411,6 +2415,7 @@ function NetworkMapContent() {
|
|||||||
dl={merged.dlMbps}
|
dl={merged.dlMbps}
|
||||||
ul={merged.ulMbps}
|
ul={merged.ulMbps}
|
||||||
onOpen={(ev) => openWanJhSpeedDetail(ev, edge)}
|
onOpen={(ev) => openWanJhSpeedDetail(ev, edge)}
|
||||||
|
probe={sp}
|
||||||
rttFromMonitor={sp?.lastPingRttMs != null}
|
rttFromMonitor={sp?.lastPingRttMs != null}
|
||||||
throughputFromMonitor={
|
throughputFromMonitor={
|
||||||
sp?.lastTxAvgMbps != null || sp?.lastRxAvgMbps != null
|
sp?.lastTxAvgMbps != null || sp?.lastRxAvgMbps != null
|
||||||
@@ -2418,7 +2423,7 @@ function NetworkMapContent() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <PingBadge mx={mx} my={my} ping={edge.pingMs} color={pingColor(edge.pingMs)} />
|
return <PingBadge mx={mx} my={my} ping={useLiveData?null:edge.pingMs} color={pingColor(useLiveData?null:edge.pingMs)} />
|
||||||
})()}
|
})()}
|
||||||
{showNetflow && hopHasRate(wanFlow) && (
|
{showNetflow && hopHasRate(wanFlow) && (
|
||||||
<NetflowRateBadge
|
<NetflowRateBadge
|
||||||
@@ -2744,7 +2749,7 @@ function NetworkMapContent() {
|
|||||||
<line x1="38" y1="4" x2="60" y2="4" stroke={c}
|
<line x1="38" y1="4" x2="60" y2="4" stroke={c}
|
||||||
strokeWidth={i === 0 ? 2 : 1.2} strokeDasharray={i === 0 ? "none" : "4 3"} opacity="0.8" />
|
strokeWidth={i === 0 ? 2 : 1.2} strokeDasharray={i === 0 ? "none" : "4 3"} opacity="0.8" />
|
||||||
<text x="66" y="8" fontSize="7.5" fill="#475569" fontFamily="system-ui">
|
<text x="66" y="8" fontSize="7.5" fill="#475569" fontFamily="system-ui">
|
||||||
{i === 0 ? "primary" : "backup"}
|
{useLiveData?"модель":i === 0 ? "primary (демо)" : "backup (демо)"}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
))}
|
))}
|
||||||
@@ -2901,7 +2906,7 @@ function NetworkMapContent() {
|
|||||||
<div className="flex flex-col gap-0">
|
<div className="flex flex-col gap-0">
|
||||||
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{rttMon ? "Мониторинг (ping/BT)" : "Модель RTT"}
|
{rttMon ? "Мониторинг (ping/BT)" : useLiveData?"Ping неизвестен":"Модель RTT (демо)"}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-xs font-mono font-semibold"
|
className="text-xs font-mono font-semibold"
|
||||||
@@ -2912,11 +2917,11 @@ function NetworkMapContent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{bwMon ? "TX / RX (BT)" : "Скорость канала (модель)"}
|
{bwMon ? "TX / RX (BT)" : useLiveData?"TX / RX неизвестны":"Скорость канала (демо)"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-mono font-semibold text-sky-400">
|
<span className="text-xs font-mono font-semibold text-sky-400">
|
||||||
{merged.dlMbps != null && merged.ulMbps != null
|
{(merged.dlMbps != null || merged.ulMbps != null)
|
||||||
? `↓${merged.dlMbps} ↑${merged.ulMbps}`
|
? `↓${merged.dlMbps??"—"} ↑${merged.ulMbps??"—"}`
|
||||||
: "—"}
|
: "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -2935,8 +2940,9 @@ function NetworkMapContent() {
|
|||||||
<p className="text-[10px] text-muted-foreground pt-2 leading-snug">
|
<p className="text-[10px] text-muted-foreground pt-2 leading-snug">
|
||||||
{merged.hasSpeedMonitor
|
{merged.hasSpeedMonitor
|
||||||
? "Ping и/или TX/RX — с последнего прогона speed-пробы; проба сопоставляется с этим GRE по WAN и интерфейсам. "
|
? "Ping и/или TX/RX — с последнего прогона speed-пробы; проба сопоставляется с этим GRE по WAN и интерфейсам. "
|
||||||
: "«Модель RTT» и «скорость канала» — демо до появления подходящей speed-пробы в «Мониторинг → скорость». "}
|
: useLiveData?"Измерения GRE неизвестны: нет подходящей пробы. ":"Измерения демонстрационные. "}
|
||||||
Поток — живой NetFlow за 5 мин (как в «Трафик»: без overlay/mesh), не ёмкость канала.
|
{spGr?"Проба "+spGr.id+" · время Ping: "+(spGr.lastPingAt?new Date(spGr.lastPingAt).toLocaleString("ru-RU"):"неизвестно")+" · время TX/RX неизвестно. ":""}
|
||||||
|
Поток — снимок NetFlow за 5 мин (как в «Трафик»: без overlay/mesh), не ёмкость канала.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3267,7 +3273,7 @@ function NetworkMapContent() {
|
|||||||
["Модель", selected.model],
|
["Модель", selected.model],
|
||||||
["RouterOS", selected.os],
|
["RouterOS", selected.os],
|
||||||
...(selected.type !== "home-router"
|
...(selected.type !== "home-router"
|
||||||
? [["ASN", selected.asn], ["Сессий", String(selected.sessions)]]
|
? [["ASN", selected.asn], ["Сессий", useLiveData?(sessionCounts[selected.id]==null?"—":String(sessionCounts[selected.id])):String(selected.sessions)]]
|
||||||
: [["LAN", selected.lanSubnet ?? "—"]]),
|
: [["LAN", selected.lanSubnet ?? "—"]]),
|
||||||
].map(([k, v]) => (
|
].map(([k, v]) => (
|
||||||
<div key={k} className="flex items-center justify-between py-2 border-b border-border/50 last:border-0">
|
<div key={k} className="flex items-center justify-between py-2 border-b border-border/50 last:border-0">
|
||||||
@@ -3278,7 +3284,7 @@ function NetworkMapContent() {
|
|||||||
<div className="flex items-center justify-between py-2">
|
<div className="flex items-center justify-between py-2">
|
||||||
<span className="text-xs text-muted-foreground">Задержка</span>
|
<span className="text-xs text-muted-foreground">Задержка</span>
|
||||||
<span className="text-xs font-mono font-semibold" style={{ color: latencyColor(selected.latency) }}>
|
<span className="text-xs font-mono font-semibold" style={{ color: latencyColor(selected.latency) }}>
|
||||||
{selected.latency == null ? "— офлайн" : `${selected.latency} мс`}
|
{selected.latency == null ? "—" : `${selected.latency} мс`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3310,7 +3316,7 @@ function NetworkMapContent() {
|
|||||||
"ml-auto text-[10px] font-medium px-1.5 py-0.5 rounded-full",
|
"ml-auto text-[10px] font-medium px-1.5 py-0.5 rounded-full",
|
||||||
isActive ? "bg-emerald-500/10 text-emerald-400" : "bg-muted text-muted-foreground",
|
isActive ? "bg-emerald-500/10 text-emerald-400" : "bg-muted text-muted-foreground",
|
||||||
)}>
|
)}>
|
||||||
{isActive ? "primary" : "backup"}
|
{useLiveData?"Аплинк каталога":isActive ? "primary (демо)" : "backup (демо)"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -3339,7 +3345,7 @@ function NetworkMapContent() {
|
|||||||
{myEdges.length > 0 && (
|
{myEdges.length > 0 && (
|
||||||
<div className="mt-2 pt-2 border-t border-border/40">
|
<div className="mt-2 pt-2 border-t border-border/40">
|
||||||
<p className="text-[9px] uppercase text-muted-foreground tracking-wider mb-1.5">
|
<p className="text-[9px] uppercase text-muted-foreground tracking-wider mb-1.5">
|
||||||
Подключения к JH
|
Модель связей с JH · Ping модели
|
||||||
</p>
|
</p>
|
||||||
{myEdges.map((e) => {
|
{myEdges.map((e) => {
|
||||||
const jh = mapServers.find(s => s.id === e.jhId)
|
const jh = mapServers.find(s => s.id === e.jhId)
|
||||||
@@ -3405,7 +3411,7 @@ function NetworkMapContent() {
|
|||||||
"text-[10px] font-medium px-1.5 py-0.5 rounded-full",
|
"text-[10px] font-medium px-1.5 py-0.5 rounded-full",
|
||||||
isPrimary ? "bg-emerald-500/10 text-emerald-400" : "bg-muted text-muted-foreground",
|
isPrimary ? "bg-emerald-500/10 text-emerald-400" : "bg-muted text-muted-foreground",
|
||||||
)}>
|
)}>
|
||||||
{isPrimary ? "primary" : "backup"}
|
{isPrimary ? "Основная на схеме" : "Дополнительная на схеме"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-medium" style={{ color: ts.stroke }}>
|
<span className="text-[10px] font-medium" style={{ color: ts.stroke }}>
|
||||||
{t.status === "up" ? "Up" : t.status === "degraded" ? "Degraded" : "Down"}
|
{t.status === "up" ? "Up" : t.status === "degraded" ? "Degraded" : "Down"}
|
||||||
@@ -3418,7 +3424,7 @@ function NetworkMapContent() {
|
|||||||
</span>
|
</span>
|
||||||
{t.ipsec
|
{t.ipsec
|
||||||
? <span className="text-[10px] text-emerald-400 flex items-center gap-0.5"><LockIcon className="size-2.5" />IPsec</span>
|
? <span className="text-[10px] text-emerald-400 flex items-center gap-0.5"><LockIcon className="size-2.5" />IPsec</span>
|
||||||
: <span className="text-[10px] text-muted-foreground flex items-center gap-0.5"><LockOpenIcon className="size-2.5" />Plain</span>}
|
: <span className="text-[10px] text-muted-foreground flex items-center gap-0.5"><LockOpenIcon className="size-2.5" />{useLiveData?"IPsec неизвестен":"Plain (демо)"}</span>}
|
||||||
</div>
|
</div>
|
||||||
{showMetrics && (
|
{showMetrics && (
|
||||||
<div className="flex items-center justify-between pt-1 border-t border-border/30">
|
<div className="flex items-center justify-between pt-1 border-t border-border/30">
|
||||||
|
|||||||
@@ -26,11 +26,19 @@
|
|||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.49; оптимизатор принят локально, карта сети в работе |
|
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.50; оптимизатор принят локально, карта сети в работе |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 10.50 — актуальные детали и модель связности карты (2026-10-01)
|
||||||
|
|
||||||
|
- Открытые детали server/GRE заново разрешаются по стабильной идентичности в текущем снимке: свежие name/status/resources не остаются на сохранённом объекте. Unknown latency/sessions/IPsec выводятся как неизвестность, не offline/0/Plain. Primary/backup GRE — только приоритет визуализации; WAN API — аплинк каталога.
|
||||||
|
- Пунктир WAN→JH прямо обозначен моделью связности. Live модель не анимируется как активный маршрут и не показывает сумму latency как RTT линии; Ping модели подписан отдельно в деталях. Probe значения сохраняют нули/частичные DL/UL; подпись показывает probe ID, дату Ping и неизвестное время скорости.
|
||||||
|
- Узлы всех ролей получили имя устройства и keyboard Enter/Space. Пульсация размера нарушала стабильность области кнопки; удалена, имя видно и для JH/Exit. Прежние React set-state/refs/immutability/exhaustive-deps и prefer-const устранены без подавления lint.
|
||||||
|
- 24 E2E карты, TypeScript и lint карты/helpers/tests прошли. Новая проверка подтверждает 0 Ping, дробную DL при неизвестной UL и старую дату пробы. Реальная сеть остаётся внешней приёмкой.
|
||||||
|
- Далее URL-контекст, клавиатурные действия остальных объектов и адаптивность/Frame/screenshots карты.
|
||||||
|
|
||||||
## Блок 10.49 — поздние ответы при смене источника карты (2026-10-01)
|
## Блок 10.49 — поздние ответы при смене источника карты (2026-10-01)
|
||||||
|
|
||||||
- Добавлена отдельная source-конфигурация Playwright (:3101, исходный backend :8000 и новый :8101). 12 сценариев snapshot/Flow/DNS × backend/mock × успех/отказ подтверждают, что прежняя карта, сервис и ошибка не попадают в новый источник, а старый ответ не запускает дополнительные чтения.
|
- Добавлена отдельная source-конфигурация Playwright (:3101, исходный backend :8000 и новый :8101). 12 сценариев snapshot/Flow/DNS × backend/mock × успех/отказ подтверждают, что прежняя карта, сервис и ошибка не попадают в новый источник, а старый ответ не запускает дополнительные чтения.
|
||||||
|
|||||||
@@ -267,3 +267,12 @@ Shared контракт и backend observed исходные значения/д
|
|||||||
- Все 12 E2E и lint новых тестов/config прошли. Default suite исключает source-файл; Next серверы запускаются последовательно.
|
- Все 12 E2E и lint новых тестов/config прошли. Default suite исключает source-файл; Next серверы запускаются последовательно.
|
||||||
- Далее выбранные детали из текущего снимка, подписи модельных WAN связей и прежние lint ошибки карты; затем URL, клавиатура, телефон и визуальная приёмка.
|
- Далее выбранные детали из текущего снимка, подписи модельных WAN связей и прежние lint ошибки карты; затем URL, клавиатура, телефон и визуальная приёмка.
|
||||||
|
|
||||||
|
|
||||||
|
## Блок 10.50 — актуальные детали и модель связности карты (2026-10-01)
|
||||||
|
|
||||||
|
- Открытые детали server/GRE заново разрешаются по стабильной идентичности в текущем снимке: свежие name/status/resources не остаются на сохранённом объекте. Unknown latency/sessions/IPsec выводятся как неизвестность, не offline/0/Plain. Primary/backup GRE — только приоритет визуализации; WAN API — аплинк каталога.
|
||||||
|
- Пунктир WAN→JH прямо обозначен моделью связности. Live модель не анимируется как активный маршрут и не показывает сумму latency как RTT линии; Ping модели подписан отдельно в деталях. Probe значения сохраняют нули/частичные DL/UL; подпись показывает probe ID, дату Ping и неизвестное время скорости.
|
||||||
|
- Узлы всех ролей получили имя устройства и keyboard Enter/Space. Пульсация размера нарушала стабильность области кнопки; удалена, имя видно и для JH/Exit. Прежние React set-state/refs/immutability/exhaustive-deps и prefer-const устранены без подавления lint.
|
||||||
|
- 24 E2E карты, TypeScript и lint карты/helpers/tests прошли. Новая проверка подтверждает 0 Ping, дробную DL при неизвестной UL и старую дату пробы. Реальная сеть остаётся внешней приёмкой.
|
||||||
|
- Далее URL-контекст, клавиатурные действия остальных объектов и адаптивность/Frame/screenshots карты.
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export interface GreSpeedProbeSnapshot {
|
|||||||
dstInterface?: string
|
dstInterface?: string
|
||||||
lastTxAvgMbps?: number | null
|
lastTxAvgMbps?: number | null
|
||||||
lastRxAvgMbps?: number | null
|
lastRxAvgMbps?: number | null
|
||||||
|
lastPingAt?: string | null
|
||||||
lastPingRttMs?: number | null
|
lastPingRttMs?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -826,8 +826,8 @@ export function buildGreMapEdges(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const t of tunnels) {
|
for (const t of tunnels) {
|
||||||
let a = servers.find((s) => s.id === t.serverId)
|
const a = servers.find((s) => s.id === t.serverId)
|
||||||
let b = findServerByGreRemote(servers, t.remoteAddress, resolvedIpv4ByHost)
|
const b = findServerByGreRemote(servers, t.remoteAddress, resolvedIpv4ByHost)
|
||||||
if (!a || !b) continue
|
if (!a || !b) continue
|
||||||
if (a.id === b.id) continue
|
if (a.id === b.id) continue
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {test,expect} from "@playwright/test"
|
||||||
|
const empty={"hops":[],"live":false,"rangeMinutes":5,"windowSec":300,"totalBytes":0,"namedBytes":0,"unclassifiedBytes":0,"asnLoaded":false,"countryLoaded":false,"services":[],"serviceEdges":[],"servicePaths":[],"countries":[],"countryEdges":[],"countryPaths":[],"countryServiceGroups":[],"mapServiceMinSharePct":5,"dedupApplied":false,"excludeMeshApplied":false,"excludeOverlayApplied":false}
|
||||||
|
const wan={id:"w",name:"WAN",iface:"ether1",isp:"ISP",ip:"192.0.2.1",maxDl:100,maxUl:100}
|
||||||
|
const home={id:1,name:"Домашний",host:"192.0.2.1",type:"home-router",country:"RU",enabled:true,status:"online",latency:2,wanUplinks:[wan]}
|
||||||
|
const jh={id:2,name:"Jump",host:"192.0.2.2",type:"jump-host",country:"RU",enabled:true,status:"online",latency:3}
|
||||||
|
const tunnel={id:"t1",serverId:"1",name:"gre-home",localAddress:"192.0.2.1",remoteAddress:"192.0.2.2",localInnerIp:"10.0.0.1",remoteInnerIp:"10.0.0.2",poolId:"",ipsec:null,mtu:1476,keepaliveInterval:10,keepaliveRetries:3,dscp:"inherit",clampTcpMss:true,allowFastPath:false,comment:"",enabled:true,status:"up"}
|
||||||
|
test.beforeEach(async({page})=>{await page.route("**/health",r=>r.fulfill({json:{ok:true}}))})
|
||||||
|
test("Карта: открытые детали обновляются вместе со снимком",async({page})=>{
|
||||||
|
let changed=false
|
||||||
|
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/servers"?[{...jh,name:changed?"Новый Jump":"Jump",status:changed?null:"online",cpuLoad:changed?0:20}]:p==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:p==="/api/uptime/speed-probes"?{probes:[]}:p==="/api/traffic/flow/map-hops"?empty:p==="/api/auth/config"?{required:false}:{}})})
|
||||||
|
await page.goto("/network-map");await page.getByRole("button",{name:"Устройство Jump",exact:true}).click()
|
||||||
|
await expect(page.getByText("20%",{exact:true})).toBeVisible()
|
||||||
|
const sessions=page.getByText("Сессий",{exact:true}).locator("..");await expect(sessions).toHaveText("Сессий—")
|
||||||
|
changed=true;await page.getByRole("button",{name:"Обновить",exact:true}).click()
|
||||||
|
await expect(page.getByText("0%",{exact:true})).toBeVisible();await expect(page.locator("span").filter({hasText:/^Неизвестно$/}).first()).toBeVisible()
|
||||||
|
await expect(page.getByText("Jump",{exact:true})).toHaveCount(0);await expect(page.getByText("Новый Jump",{exact:true}).last()).toBeVisible()
|
||||||
|
})
|
||||||
|
test("Карта: модель WAN не утверждает активный маршрут и RTT линии",async({page})=>{
|
||||||
|
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/servers"?[home,jh]:p==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:p==="/api/uptime/speed-probes"?{probes:[]}:p==="/api/traffic/flow/map-hops"?empty:p==="/api/auth/config"?{required:false}:{}})})
|
||||||
|
await page.goto("/network-map");await expect(page.getByText("Пунктир WAN→JH — модель связности; маршрут не подтверждён.",{exact:true})).toBeVisible()
|
||||||
|
await expect(page.locator("svg text").filter({hasText:/^5 мс$/})).toHaveCount(0)
|
||||||
|
await page.getByText("Домашний",{exact:true}).first().click()
|
||||||
|
await expect(page.getByText("Аплинк каталога",{exact:true})).toBeVisible();await expect(page.getByText("Модель связей с JH · Ping модели",{exact:true})).toBeVisible()
|
||||||
|
await expect(page.getByText("primary",{exact:true})).toHaveCount(0);await expect(page.getByText("backup",{exact:true})).toHaveCount(0)
|
||||||
|
})
|
||||||
|
test("Карта: выбранный GRE обновляется и неизвестный IPsec не означает Plain",async({page})=>{
|
||||||
|
let changed=false
|
||||||
|
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/servers"?[home,jh]:p==="/api/filters/gre-tunnels"?{tunnels:[{...tunnel,name:changed?"gre-new":"gre-home",status:changed?"down":"up"}],failures:[]}:p==="/api/uptime/speed-probes"?{probes:[]}:p==="/api/traffic/flow/map-hops"?empty:p==="/api/auth/config"?{required:false}:{}})})
|
||||||
|
await page.goto("/network-map")
|
||||||
|
await page.locator("g").filter({has:page.locator("title",{hasText:"RTT неизвестен."})}).last().click()
|
||||||
|
await expect(page.getByText("gre-home",{exact:true}).last()).toBeVisible();await expect(page.getByText("Ping неизвестен",{exact:true})).toBeVisible()
|
||||||
|
changed=true;await page.getByRole("button",{name:"Обновить",exact:true}).click()
|
||||||
|
await expect(page.getByText("gre-new",{exact:true}).last()).toBeVisible();await expect(page.getByText("Недоступен",{exact:true})).toBeVisible()
|
||||||
|
await page.getByText("Домашний",{exact:true}).first().click();await expect(page.getByText("IPsec неизвестен",{exact:true})).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Карта: GRE сохраняет частичное измерение, ноль Ping и дату пробы",async({page})=>{
|
||||||
|
const probe={id:"measured-probe",srcServerId:"1",dstServerId:"2",srcInterface:"gre-home",dstInterface:"",enabled:true,lastPingRttMs:0,lastPingLossPct:0,lastTxAvgMbps:0.125,lastRxAvgMbps:null,lastPingAt:"2020-01-01T00:00:00Z"}
|
||||||
|
await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/servers"?[home,jh]:p==="/api/filters/gre-tunnels"?{tunnels:[tunnel],failures:[]}:p==="/api/uptime/speed-probes"?{probes:[probe]}:p==="/api/traffic/flow/map-hops"?empty:p==="/api/auth/config"?{required:false}:{}})})
|
||||||
|
await page.goto("/network-map")
|
||||||
|
await page.locator("g").filter({has:page.locator("title",{hasText:"Проба measured-probe"})}).last().click()
|
||||||
|
await expect(page.getByText("0 мс",{exact:true}).last()).toBeVisible()
|
||||||
|
await expect(page.getByText("↓0.125 ↑—",{exact:true}).last()).toBeVisible()
|
||||||
|
await expect(page.getByText("Проба measured-probe",{exact:false}).last()).toContainText("2020")
|
||||||
|
await expect(page.getByText("Проба measured-probe",{exact:false}).last()).toContainText("время TX/RX неизвестно")
|
||||||
|
})
|
||||||
Reference in new issue
Block a user