fix(network-map): рисовать пунктир и скорость до сервиса
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m41s
Docker images / frontend-image (push) Successful in 2m55s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 43s
Docker images / publish-release (push) Successful in 9s
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m41s
Docker images / frontend-image (push) Successful in 2m55s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 43s
Docker images / publish-release (push) Successful in 9s
Якорь hop на EN даже без nextHop; линия ARN\to Google с NetflowRateBadge. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+132
-52
@@ -293,6 +293,85 @@ function serviceSharePct(share: number): string {
|
||||
return `${Math.round(share * 100)}%`
|
||||
}
|
||||
|
||||
function exitNodeIdsFromGre(greEdges: GreMapEdge[]): string[] {
|
||||
const ids = new Set<string>()
|
||||
for (const g of greEdges) {
|
||||
if (g.fromServer.type === "exit-node") ids.add(g.fromServer.id)
|
||||
if (g.toServer.type === "exit-node") ids.add(g.toServer.id)
|
||||
}
|
||||
return [...ids]
|
||||
}
|
||||
|
||||
function remapServiceEdgeFromId(
|
||||
fromId: string,
|
||||
servers: Server[],
|
||||
greEdges: GreMapEdge[],
|
||||
soleEnId: string | null,
|
||||
): string | null {
|
||||
const srv = servers.find((s) => s.id === fromId)
|
||||
if (srv?.type === "exit-node") return fromId
|
||||
for (const g of greEdges) {
|
||||
if (g.fromServer.id === fromId && g.toServer.type === "exit-node") return g.toServer.id
|
||||
if (g.toServer.id === fromId && g.fromServer.type === "exit-node") return g.fromServer.id
|
||||
}
|
||||
return soleEnId
|
||||
}
|
||||
|
||||
/** API-рёбра на EN; если hop нет — синтез от единственного EN на карте (GRE / каталог). */
|
||||
function drawableServiceEdges(
|
||||
services: FlowMapService[],
|
||||
edges: FlowMapServiceEdge[],
|
||||
servers: Server[],
|
||||
greEdges: GreMapEdge[],
|
||||
nodePosById: Record<string, { x: number; y: number }>,
|
||||
): FlowMapServiceEdge[] {
|
||||
const keep = new Set(services.map((s) => s.id))
|
||||
const greEn = exitNodeIdsFromGre(greEdges)
|
||||
const catalogEn = servers.filter((s) => s.type === "exit-node").map((s) => s.id)
|
||||
const enPool = greEn.length > 0 ? greEn : catalogEn
|
||||
const soleEnId = enPool.length === 1 ? enPool[0]! : null
|
||||
const merged = new Map<string, FlowMapServiceEdge>()
|
||||
|
||||
function bump(e: FlowMapServiceEdge) {
|
||||
const key = `${e.fromId}|${e.toId}`
|
||||
const prev = merged.get(key)
|
||||
if (!prev) {
|
||||
merged.set(key, { ...e })
|
||||
return
|
||||
}
|
||||
merged.set(key, {
|
||||
...prev,
|
||||
bytes: prev.bytes + e.bytes,
|
||||
bps: prev.bps + e.bps,
|
||||
bpsFwd: prev.bpsFwd + e.bpsFwd,
|
||||
bpsRev: prev.bpsRev + e.bpsRev,
|
||||
})
|
||||
}
|
||||
|
||||
for (const e of edges) {
|
||||
if (!keep.has(e.toId)) continue
|
||||
const fromId = remapServiceEdgeFromId(e.fromId, servers, greEdges, soleEnId)
|
||||
if (!fromId || !nodePosById[fromId]) continue
|
||||
bump({ ...e, fromId })
|
||||
}
|
||||
|
||||
const covered = new Set([...merged.values()].map((e) => e.toId))
|
||||
if (soleEnId && nodePosById[soleEnId]) {
|
||||
for (const svc of services) {
|
||||
if (covered.has(svc.id)) continue
|
||||
bump({
|
||||
fromId: soleEnId,
|
||||
toId: svc.id,
|
||||
bytes: svc.bytes,
|
||||
bps: svc.bps,
|
||||
bpsFwd: svc.bps,
|
||||
bpsRev: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
return [...merged.values()]
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function pingColor(ms: number | null) {
|
||||
@@ -1337,9 +1416,9 @@ export default function NetworkMapPage() {
|
||||
}, [homeRouters, wanJhEdges, mapHops, showNetflow])
|
||||
|
||||
const visibleMapServices = showServices ? mapServices : []
|
||||
const visibleServiceEdges = showServices ? mapServiceEdges.filter((e) =>
|
||||
visibleMapServices.some((s) => s.id === e.toId),
|
||||
) : []
|
||||
const visibleServiceEdges = showServices
|
||||
? drawableServiceEdges(visibleMapServices, mapServiceEdges, mapServers, greEdges, nodePosById)
|
||||
: []
|
||||
|
||||
const nodes = mapServers
|
||||
.map((s) => ({ ...s, ...nodePosById[s.id]! }))
|
||||
@@ -2019,55 +2098,6 @@ export default function NetworkMapPage() {
|
||||
)
|
||||
})}
|
||||
|
||||
{/* ── EN → destination services ── */}
|
||||
{visibleServiceEdges.map((edge) => {
|
||||
const from = nodeById[edge.fromId] ?? nodePosById[edge.fromId]
|
||||
const to = servicePosById[edge.toId]
|
||||
if (!from || !to) return null
|
||||
const hop: MatchedNetflowHop = {
|
||||
bytes: edge.bytes,
|
||||
bps: edge.bps,
|
||||
bpsFwd: edge.bpsFwd,
|
||||
bpsRev: edge.bpsRev,
|
||||
}
|
||||
const { mx, my } = edgeBadgePosition(from.x, from.y, to.x, to.y, 0.55, 16)
|
||||
const hl = selectedService?.id === edge.toId || selected?.id === edge.fromId
|
||||
const svc = visibleMapServices.find((s) => s.id === edge.toId)
|
||||
const enName = mapServers.find((s) => s.id === edge.fromId)?.name ?? edge.fromId
|
||||
const clientLabel = (edge.clients?.map((c) => c.name).filter(Boolean).join(", ") || edge.clientName || "—")
|
||||
const pathTitle = `${clientLabel} → ${enName} → ${svc?.label ?? edge.toId}`
|
||||
return (
|
||||
<g key={`${edge.fromId}|${edge.toId}`} opacity={hl ? 1 : 0.72} style={{ transition: "opacity 0.3s" }}>
|
||||
<title>{pathTitle}</title>
|
||||
<line
|
||||
x1={from.x} y1={from.y} x2={to.x} y2={to.y}
|
||||
stroke="#22d3ee"
|
||||
strokeWidth={hopHasRate(hop) ? 2.2 : 1.3}
|
||||
strokeDasharray="5 5"
|
||||
opacity="0.7"
|
||||
/>
|
||||
{showAnimDots && hopHasRate(hop) && (
|
||||
<circle r="3" fill="#67e8f9" opacity="0.85" pointerEvents="none">
|
||||
<animateMotion dur="2.6s" repeatCount="indefinite"
|
||||
path={`M ${from.x} ${from.y} L ${to.x} ${to.y}`} />
|
||||
</circle>
|
||||
)}
|
||||
{hopHasRate(hop) && (
|
||||
<NetflowRateBadge
|
||||
mx={mx}
|
||||
my={my}
|
||||
hop={hop}
|
||||
onOpen={(ev) => {
|
||||
ev.stopPropagation()
|
||||
const svc = visibleMapServices.find((s) => s.id === edge.toId)
|
||||
if (svc) selectService(svc)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* ── Server nodes ── */}
|
||||
{nodes.map(n => (
|
||||
<ServerNode
|
||||
@@ -2136,6 +2166,56 @@ export default function NetworkMapPage() {
|
||||
)
|
||||
})}
|
||||
|
||||
{/* ── EN → destination services (поверх узлов, чтобы пунктир не прятался) ── */}
|
||||
{visibleServiceEdges.map((edge) => {
|
||||
const from = nodeById[edge.fromId] ?? nodePosById[edge.fromId]
|
||||
const to = servicePosById[edge.toId]
|
||||
if (!from || !to) return null
|
||||
const hop: MatchedNetflowHop = {
|
||||
bytes: edge.bytes,
|
||||
bps: edge.bps,
|
||||
bpsFwd: edge.bpsFwd,
|
||||
bpsRev: edge.bpsRev,
|
||||
}
|
||||
const { mx, my } = edgeBadgePosition(from.x, from.y, to.x, to.y, 0.55, 16)
|
||||
const hl = selectedService?.id === edge.toId || selected?.id === edge.fromId
|
||||
const svc = visibleMapServices.find((s) => s.id === edge.toId)
|
||||
const enName = mapServers.find((s) => s.id === edge.fromId)?.name ?? edge.fromId
|
||||
const clientLabel = (edge.clients?.map((c) => c.name).filter(Boolean).join(", ") || edge.clientName || "—")
|
||||
const pathTitle = `${clientLabel} → ${enName} → ${svc?.label ?? edge.toId}`
|
||||
return (
|
||||
<g key={`${edge.fromId}|${edge.toId}`} opacity={hl ? 1 : 0.72} style={{ transition: "opacity 0.3s" }}>
|
||||
<title>{pathTitle}</title>
|
||||
<line
|
||||
x1={from.x} y1={from.y} x2={to.x} y2={to.y}
|
||||
stroke="#22d3ee"
|
||||
strokeWidth={hopHasRate(hop) ? 2.4 : 1.4}
|
||||
strokeDasharray="6 5"
|
||||
opacity="0.85"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
{showAnimDots && hopHasRate(hop) && (
|
||||
<circle r="3" fill="#67e8f9" opacity="0.85" pointerEvents="none">
|
||||
<animateMotion dur="2.6s" repeatCount="indefinite"
|
||||
path={`M ${from.x} ${from.y} L ${to.x} ${to.y}`} />
|
||||
</circle>
|
||||
)}
|
||||
{hopHasRate(hop) && (
|
||||
<NetflowRateBadge
|
||||
mx={mx}
|
||||
my={my}
|
||||
hop={hop}
|
||||
onOpen={(ev) => {
|
||||
ev.stopPropagation()
|
||||
const svc = visibleMapServices.find((s) => s.id === edge.toId)
|
||||
if (svc) selectService(svc)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* ── Hover tooltip ── */}
|
||||
{hoveredNode && !isDragging && (
|
||||
<SvgTooltip n={hoveredNode} />
|
||||
|
||||
Reference in New Issue
Block a user