feat(traffic-flow): enhance country handling in flow analytics
Docker images / prepare-release (push) Successful in 9s
Docker images / backend-test (push) Successful in 2m19s
Docker images / frontend-image (push) Successful in 3m26s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m23s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
Docker images / prepare-release (push) Successful in 9s
Docker images / backend-test (push) Successful in 2m19s
Docker images / frontend-image (push) Successful in 3m26s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m23s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
- Introduced country-specific handling in traffic flow analytics, including new mappings for country nodes and edges. - Added mock data for countries and their respective service paths to improve visualization in the Network Map. - Updated the `countryName` function to utilize `Intl.DisplayNames` for better localization of country names. - Enhanced tests to validate country handling and ensure accurate representation in flow analytics. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+160
-21
@@ -57,6 +57,7 @@ import {
|
||||
} from "@/lib/map-netflow-hops"
|
||||
import type { FlowMapHop, FlowMapHopsDto, FlowMapService, FlowMapServiceEdge, FlowMapServicePath } from "@mmapp/contracts/traffic-flow"
|
||||
import { ServiceBrandIcon } from "@/components/network-map/service-brand-icon"
|
||||
import { CountryFlagSvg } from "@/components/network-map/country-flag-svg"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { StatusBadge } from "@/components/status-badge"
|
||||
import { StatusDot } from "@/components/status-dot"
|
||||
@@ -69,7 +70,7 @@ import {
|
||||
import { cn } from "@/lib/utils"
|
||||
import { formatServicePathLabel, formatServicePathTitle } from "@/lib/format-service-path-label"
|
||||
import Link from "next/link"
|
||||
import { Flag } from "@/components/flag"
|
||||
import { Flag, countryName } from "@/components/flag"
|
||||
|
||||
// ─── Resource metrics (для мини-блока справа; числа детерминированы по id узла) ─
|
||||
|
||||
@@ -285,6 +286,12 @@ const MOCK_MAP_SERVICES: FlowMapService[] = [
|
||||
{ id: "svc:aws", label: "AWS", category: "CDN", bytes: 9_000_000, bps: 3_600_000, share: 0.09 },
|
||||
]
|
||||
|
||||
const MOCK_MAP_COUNTRIES: FlowMapService[] = [
|
||||
{ id: "cc:us", label: "US", category: "Страна", bytes: 22_000_000, bps: 8_800_000, share: 0.38 },
|
||||
{ id: "cc:nl", label: "NL", category: "Страна", bytes: 14_000_000, bps: 5_600_000, share: 0.31 },
|
||||
{ id: "cc:de", label: "DE", category: "Страна", bytes: 9_000_000, bps: 3_600_000, share: 0.21 },
|
||||
]
|
||||
|
||||
const MOCK_MAP_SERVICE_EDGES: FlowMapServiceEdge[] = [
|
||||
{ fromId: "srv2", toId: "svc:google", bytes: 14_000_000, bps: 5_600_000, bpsFwd: 4_200_000, bpsRev: 1_400_000, clientName: "Alice", clients: [{ id: "u1", name: "Alice" }] },
|
||||
{ fromId: "srv3", toId: "svc:google", bytes: 8_000_000, bps: 3_200_000, bpsFwd: 2_400_000, bpsRev: 800_000, clientName: "Bob", clients: [{ id: "u2", name: "Bob" }] },
|
||||
@@ -293,6 +300,14 @@ const MOCK_MAP_SERVICE_EDGES: FlowMapServiceEdge[] = [
|
||||
{ fromId: "srv3", toId: "svc:aws", bytes: 9_000_000, bps: 3_600_000, bpsFwd: 2_700_000, bpsRev: 900_000, clientName: "Bob", clients: [{ id: "u2", name: "Bob" }] },
|
||||
]
|
||||
|
||||
const MOCK_MAP_COUNTRY_EDGES: FlowMapServiceEdge[] = [
|
||||
{ fromId: "srv2", toId: "cc:us", bytes: 14_000_000, bps: 5_600_000, bpsFwd: 4_200_000, bpsRev: 1_400_000, clientName: "Alice", clients: [{ id: "u1", name: "Alice" }] },
|
||||
{ fromId: "srv3", toId: "cc:us", bytes: 8_000_000, bps: 3_200_000, bpsFwd: 2_400_000, bpsRev: 800_000, clientName: "Bob", clients: [{ id: "u2", name: "Bob" }] },
|
||||
{ fromId: "srv2", toId: "cc:nl", bytes: 9_000_000, bps: 3_600_000, bpsFwd: 2_800_000, bpsRev: 800_000, clientName: "Alice", clients: [{ id: "u1", name: "Alice" }] },
|
||||
{ fromId: "srv3", toId: "cc:nl", bytes: 5_000_000, bps: 2_000_000, bpsFwd: 1_500_000, bpsRev: 500_000, clientName: "Bob", clients: [{ id: "u2", name: "Bob" }] },
|
||||
{ fromId: "srv3", toId: "cc:de", bytes: 9_000_000, bps: 3_600_000, bpsFwd: 2_700_000, bpsRev: 900_000, clientName: "Bob", clients: [{ id: "u2", name: "Bob" }] },
|
||||
]
|
||||
|
||||
const MOCK_MAP_SERVICE_PATHS: FlowMapServicePath[] = [
|
||||
{ clientId: "u1", clientName: "Alice", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv2", enName: "mt-spb-edge-01", serviceId: "svc:google", bytes: 14_000_000, bps: 5_600_000 },
|
||||
{ clientId: "u2", clientName: "Bob", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv3", enName: "mt-fra-edge-01", serviceId: "svc:google", bytes: 8_000_000, bps: 3_200_000 },
|
||||
@@ -301,6 +316,33 @@ const MOCK_MAP_SERVICE_PATHS: FlowMapServicePath[] = [
|
||||
{ clientId: "u2", clientName: "Bob", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv3", enName: "mt-fra-edge-01", serviceId: "svc:aws", bytes: 9_000_000, bps: 3_600_000 },
|
||||
]
|
||||
|
||||
const MOCK_MAP_COUNTRY_PATHS: FlowMapServicePath[] = [
|
||||
{ clientId: "u1", clientName: "Alice", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv2", enName: "mt-spb-edge-01", serviceId: "cc:us", bytes: 14_000_000, bps: 5_600_000 },
|
||||
{ clientId: "u2", clientName: "Bob", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv3", enName: "mt-fra-edge-01", serviceId: "cc:us", bytes: 8_000_000, bps: 3_200_000 },
|
||||
{ clientId: "u1", clientName: "Alice", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv2", enName: "mt-spb-edge-01", serviceId: "cc:nl", bytes: 9_000_000, bps: 3_600_000 },
|
||||
{ clientId: "u2", clientName: "Bob", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv3", enName: "mt-fra-edge-01", serviceId: "cc:nl", bytes: 5_000_000, bps: 2_000_000 },
|
||||
{ clientId: "u2", clientName: "Bob", viaId: "srv1", viaName: "mt-msk-core-01", enId: "srv3", enName: "mt-fra-edge-01", serviceId: "cc:de", bytes: 9_000_000, bps: 3_600_000 },
|
||||
]
|
||||
|
||||
const DEST_MODE_KEY = "mm-network-map-dest-mode"
|
||||
type DestMode = "services" | "countries"
|
||||
|
||||
function readDestMode(): DestMode {
|
||||
if (typeof window === "undefined") return "services"
|
||||
try {
|
||||
return sessionStorage.getItem(DEST_MODE_KEY) === "countries" ? "countries" : "services"
|
||||
} catch {
|
||||
return "services"
|
||||
}
|
||||
}
|
||||
|
||||
function destDisplayLabel(node: FlowMapService | undefined, mode: DestMode, fallback = ""): string {
|
||||
if (!node) return fallback
|
||||
if (mode !== "countries") return node.label
|
||||
if (node.id === "cc:other" || node.label === "Прочее") return "Прочее"
|
||||
return countryName(node.label)
|
||||
}
|
||||
|
||||
function servicePathKey(p: Pick<FlowMapServicePath, "clientId" | "viaId" | "enId" | "serviceId">): string {
|
||||
return `${p.clientId}|${p.viaId}|${p.enId}|${p.serviceId}`
|
||||
}
|
||||
@@ -738,6 +780,8 @@ function ServiceNode({
|
||||
isSel,
|
||||
isVis,
|
||||
isDragged,
|
||||
destMode,
|
||||
iso,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
}: {
|
||||
@@ -748,11 +792,14 @@ function ServiceNode({
|
||||
isSel: boolean
|
||||
isVis: boolean
|
||||
isDragged: boolean
|
||||
destMode: DestMode
|
||||
iso?: string
|
||||
onClick: () => void
|
||||
onMouseDown: (e: React.MouseEvent) => void
|
||||
}) {
|
||||
const bw = MAP_SERVICE_NODE_W
|
||||
const bh = MAP_SERVICE_NODE_H
|
||||
const flagIso = destMode === "countries" && iso && iso !== "Прочее" ? iso : ""
|
||||
return (
|
||||
<g
|
||||
transform={`translate(${x},${y})`}
|
||||
@@ -786,7 +833,9 @@ function ServiceNode({
|
||||
strokeWidth={isSel ? 2.2 : 1.4}
|
||||
/>
|
||||
<g transform="translate(-11,-24)" pointerEvents="none">
|
||||
<ServiceBrandIcon label={label} size={22} />
|
||||
{flagIso
|
||||
? <CountryFlagSvg iso={flagIso} size={22} />
|
||||
: <ServiceBrandIcon label={destMode === "countries" ? "Прочее" : label} size={22} />}
|
||||
</g>
|
||||
<text textAnchor="middle" y="14" fontSize="8.5" fontWeight="700" fill="#e0f2fe" fontFamily="ui-monospace,monospace">
|
||||
{label}
|
||||
@@ -804,6 +853,7 @@ function ServicePathList({
|
||||
services,
|
||||
highlight,
|
||||
viaMode,
|
||||
destMode,
|
||||
onToggle,
|
||||
}: {
|
||||
paths: FlowMapServicePath[]
|
||||
@@ -811,6 +861,7 @@ function ServicePathList({
|
||||
services: FlowMapService[]
|
||||
highlight: { viaId: string; enId: string; serviceId: string } | null
|
||||
viaMode: "via" | "service"
|
||||
destMode: DestMode
|
||||
onToggle: (p: FlowMapServicePath) => void
|
||||
}) {
|
||||
if (paths.length === 0) {
|
||||
@@ -823,13 +874,14 @@ function ServicePathList({
|
||||
const via = servers.find((s) => s.id === p.viaId)
|
||||
const en = servers.find((s) => s.id === p.enId)
|
||||
const svc = services.find((s) => s.id === p.serviceId)
|
||||
const destLabel = destDisplayLabel(svc, destMode, p.serviceId)
|
||||
const label = formatServicePathLabel(p, viaMode, {
|
||||
viaName: via?.name,
|
||||
viaSite: via?.site,
|
||||
enName: en?.name,
|
||||
serviceLabel: svc?.label,
|
||||
serviceLabel: destLabel,
|
||||
})
|
||||
const title = formatServicePathTitle(label, svc?.label ?? p.serviceId)
|
||||
const title = formatServicePathTitle(label, destLabel)
|
||||
const active = Boolean(
|
||||
highlight
|
||||
&& highlight.viaId === p.viaId
|
||||
@@ -1099,11 +1151,15 @@ export default function NetworkMapPage() {
|
||||
const [mapServices, setMapServices] = useState<FlowMapService[]>([])
|
||||
const [mapServiceEdges, setMapServiceEdges] = useState<FlowMapServiceEdge[]>([])
|
||||
const [mapServicePaths, setMapServicePaths] = useState<FlowMapServicePath[]>([])
|
||||
const [mapCountries, setMapCountries] = useState<FlowMapService[]>([])
|
||||
const [mapCountryEdges, setMapCountryEdges] = useState<FlowMapServiceEdge[]>([])
|
||||
const [mapCountryPaths, setMapCountryPaths] = useState<FlowMapServicePath[]>([])
|
||||
const [mapSharePct, setMapSharePct] = useState(5)
|
||||
const [mapNamedBytes, setMapNamedBytes] = useState(0)
|
||||
const [mapTotalBytes, setMapTotalBytes] = useState(0)
|
||||
const [mapWindowSec, setMapWindowSec] = useState(300)
|
||||
const [mapAsnLoaded, setMapAsnLoaded] = useState(true)
|
||||
const [mapCountryLoaded, setMapCountryLoaded] = useState(true)
|
||||
/** FQDN из GRE outer → IPv4 (ответ POST /api/network/resolve-hosts), для матчинга с WAN. */
|
||||
const [greResolvedIpv4ByHost, setGreResolvedIpv4ByHost] = useState<Record<string, string>>({})
|
||||
const [dataError, setDataError] = useState<string | null>(null)
|
||||
@@ -1199,9 +1255,13 @@ export default function NetworkMapPage() {
|
||||
setMapServices(MOCK_MAP_SERVICES)
|
||||
setMapServiceEdges(MOCK_MAP_SERVICE_EDGES)
|
||||
setMapServicePaths(MOCK_MAP_SERVICE_PATHS)
|
||||
setMapCountries(MOCK_MAP_COUNTRIES)
|
||||
setMapCountryEdges(MOCK_MAP_COUNTRY_EDGES)
|
||||
setMapCountryPaths(MOCK_MAP_COUNTRY_PATHS)
|
||||
setMapSharePct(5)
|
||||
setMapNamedBytes(0)
|
||||
setMapTotalBytes(0)
|
||||
setMapCountryLoaded(true)
|
||||
setDataError(null)
|
||||
})
|
||||
return
|
||||
@@ -1230,8 +1290,21 @@ export default function NetworkMapPage() {
|
||||
// ── Interaction ─────────────────────────────────────────────────────────────
|
||||
const [selected, setSelected] = useState<Server | null>(null)
|
||||
const [selectedService, setSelectedService] = useState<FlowMapService | null>(null)
|
||||
const [destMode, setDestModeState] = useState<DestMode>("services")
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => setDestModeState(readDestMode()))
|
||||
}, [])
|
||||
function setDestMode(mode: DestMode) {
|
||||
setDestModeState(mode)
|
||||
setSelectedService(null)
|
||||
setHighlightedPath(null)
|
||||
try { sessionStorage.setItem(DEST_MODE_KEY, mode) } catch { /* private mode */ }
|
||||
}
|
||||
const liveSelectedService = selectedService
|
||||
? (mapServices.find((s) => s.id === selectedService.id) ?? selectedService)
|
||||
? (
|
||||
(destMode === "countries" ? mapCountries : mapServices)
|
||||
.find((s) => s.id === selectedService.id) ?? selectedService
|
||||
)
|
||||
: null
|
||||
const [highlightedPath, setHighlightedPath] = useState<{ viaId: string; enId: string; serviceId: string } | null>(null)
|
||||
const [selWanIdx, setSelWanIdx] = useState<number | null>(null)
|
||||
@@ -1283,7 +1356,11 @@ export default function NetworkMapPage() {
|
||||
setMapServices(MOCK_MAP_SERVICES)
|
||||
setMapServiceEdges(MOCK_MAP_SERVICE_EDGES)
|
||||
setMapServicePaths(MOCK_MAP_SERVICE_PATHS)
|
||||
setMapCountries(MOCK_MAP_COUNTRIES)
|
||||
setMapCountryEdges(MOCK_MAP_COUNTRY_EDGES)
|
||||
setMapCountryPaths(MOCK_MAP_COUNTRY_PATHS)
|
||||
setMapSharePct(5)
|
||||
setMapCountryLoaded(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1293,6 +1370,9 @@ export default function NetworkMapPage() {
|
||||
setMapServices([])
|
||||
setMapServiceEdges([])
|
||||
setMapServicePaths([])
|
||||
setMapCountries([])
|
||||
setMapCountryEdges([])
|
||||
setMapCountryPaths([])
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1308,10 +1388,14 @@ export default function NetworkMapPage() {
|
||||
setMapServices(res.services ?? [])
|
||||
setMapServiceEdges(res.serviceEdges ?? [])
|
||||
setMapServicePaths(res.servicePaths ?? [])
|
||||
setMapCountries(res.countries ?? [])
|
||||
setMapCountryEdges(res.countryEdges ?? [])
|
||||
setMapCountryPaths(res.countryPaths ?? [])
|
||||
if (res.mapServiceMinSharePct != null) setMapSharePct(res.mapServiceMinSharePct)
|
||||
setMapNamedBytes(res.namedBytes ?? 0)
|
||||
setMapTotalBytes(res.totalBytes ?? 0)
|
||||
if (res.asnLoaded != null) setMapAsnLoaded(res.asnLoaded)
|
||||
if (res.countryLoaded != null) setMapCountryLoaded(res.countryLoaded)
|
||||
if (res.windowSec) setMapWindowSec(res.windowSec)
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
@@ -1515,9 +1599,13 @@ export default function NetworkMapPage() {
|
||||
return m
|
||||
}, [homeRouters, wanJhEdges, mapHops, showNetflow])
|
||||
|
||||
const visibleMapServices = showServices ? mapServices : []
|
||||
const destNodes = destMode === "countries" ? mapCountries : mapServices
|
||||
const destEdges = destMode === "countries" ? mapCountryEdges : mapServiceEdges
|
||||
const destPaths = destMode === "countries" ? mapCountryPaths : mapServicePaths
|
||||
|
||||
const visibleMapServices = showServices ? destNodes : []
|
||||
const visibleServiceEdges = showServices
|
||||
? drawableServiceEdges(visibleMapServices, mapServiceEdges, mapServers, greEdges, nodePosById)
|
||||
? drawableServiceEdges(visibleMapServices, destEdges, mapServers, greEdges, nodePosById)
|
||||
: []
|
||||
|
||||
const nodes = mapServers
|
||||
@@ -1756,6 +1844,13 @@ export default function NetworkMapPage() {
|
||||
}
|
||||
|
||||
// ── Side panel ────────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!selectedService) return
|
||||
if (!destNodes.some((s) => s.id === selectedService.id)) {
|
||||
setSelectedService(null)
|
||||
setHighlightedPath(null)
|
||||
}
|
||||
}, [destMode, destNodes, selectedService])
|
||||
function selectServer(s: Server) {
|
||||
setSelectedGreEdge(null)
|
||||
setSelectedService(null)
|
||||
@@ -1919,6 +2014,27 @@ export default function NetworkMapPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dest overlay: services vs countries */}
|
||||
<div className="flex items-center gap-0.5 rounded-md border border-border bg-muted/40 p-0.5">
|
||||
{([
|
||||
{ value: "services" as const, label: "Сервисы" },
|
||||
{ value: "countries" as const, label: "Страны" },
|
||||
]).map((b) => (
|
||||
<button
|
||||
key={b.value}
|
||||
onClick={() => setDestMode(b.value)}
|
||||
className={cn(
|
||||
"px-2.5 py-1 text-xs rounded transition-colors whitespace-nowrap",
|
||||
destMode === b.value
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{b.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Layers dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -1937,7 +2053,7 @@ export default function NetworkMapPage() {
|
||||
{([
|
||||
{ key: "showPingBadges", label: "Ping-значки", val: showPingBadges, set: setShowPingBadges, hint: "P" },
|
||||
{ key: "showNetflow", label: "NetFlow", val: showNetflow, set: setShowNetflow, hint: "" },
|
||||
{ key: "showServices", label: "Сервисы", val: showServices, set: setShowServices, hint: "" },
|
||||
{ key: "showServices", label: "Назначения", val: showServices, set: setShowServices, hint: "" },
|
||||
{ key: "showAnimDots", label: "Анимация трафика", val: showAnimDots, set: setShowAnimDots, hint: "" },
|
||||
{ key: "showMinimap", label: "Минимап", val: showMinimap, set: setShowMinimap, hint: "M" },
|
||||
{ key: "showHints", label: "Горячие клавиши", val: showHints, set: setShowHints, hint: "" },
|
||||
@@ -1964,9 +2080,14 @@ export default function NetworkMapPage() {
|
||||
))}
|
||||
<p className="px-3 pt-1.5 pb-1 text-[10px] text-muted-foreground leading-snug">
|
||||
{mapSharePct > 0
|
||||
? `Порог доли сервиса ≥ ${mapSharePct}% · Настройки → NetFlow`
|
||||
: "Порог доли выключен (все бренды, макс. 20) · Настройки → NetFlow"}
|
||||
? `Порог доли ≥ ${mapSharePct}% · Настройки → NetFlow`
|
||||
: "Порог доли выключен (все узлы, макс. 20) · Настройки → NetFlow"}
|
||||
</p>
|
||||
{destMode === "countries" && !mapCountryLoaded && (
|
||||
<p className="px-3 pb-1 text-[10px] text-amber-500 leading-snug">
|
||||
GeoIP Country не загружен, страны из RIPE-кэша
|
||||
</p>
|
||||
)}
|
||||
{(Object.keys(nodePositions).length > 0 || Object.keys(satPositions).length > 0 || Object.keys(servicePositions).length > 0) && (
|
||||
<div className="border-t border-border/50 mt-1 pt-1">
|
||||
<button
|
||||
@@ -2286,13 +2407,15 @@ export default function NetworkMapPage() {
|
||||
return (
|
||||
<ServiceNode
|
||||
key={svc.id}
|
||||
label={svc.label}
|
||||
label={destDisplayLabel(svc, destMode)}
|
||||
share={svc.share}
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
isSel={selectedService?.id === svc.id}
|
||||
isVis
|
||||
isDragged={draggedSvcId === svc.id}
|
||||
destMode={destMode}
|
||||
iso={svc.label}
|
||||
onMouseDown={(e) => onServiceMouseDown(e, svc.id, pos.x, pos.y)}
|
||||
onClick={() => {
|
||||
if (suppressClickRef.current) { suppressClickRef.current = false; return }
|
||||
@@ -2336,7 +2459,7 @@ export default function NetworkMapPage() {
|
||||
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}`
|
||||
const pathTitle = `${clientLabel} → ${enName} → ${destDisplayLabel(svc, destMode, edge.toId)}`
|
||||
return (
|
||||
<g
|
||||
key={`${edge.fromId}|${edge.toId}`}
|
||||
@@ -2424,7 +2547,9 @@ export default function NetworkMapPage() {
|
||||
|
||||
<g transform="translate(10, 164)">
|
||||
<rect width="14" height="14" rx="4" fill="#08202c" stroke="#22d3ee" strokeWidth="1.2" />
|
||||
<text x="22" y="11" fontSize="8.5" fill="#cbd5e1" fontFamily="system-ui">Сервис</text>
|
||||
<text x="22" y="11" fontSize="8.5" fill="#cbd5e1" fontFamily="system-ui">
|
||||
{destMode === "countries" ? "Страна" : "Сервис"}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<line x1="10" y1="186" x2="130" y2="186" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />
|
||||
@@ -2727,12 +2852,18 @@ export default function NetworkMapPage() {
|
||||
<>
|
||||
<div className="flex items-start gap-2 px-4 py-3 border-b">
|
||||
<div className="mt-0.5">
|
||||
<ServiceBrandIcon label={liveSelectedService.label} size={22} />
|
||||
{destMode === "countries" && liveSelectedService.label !== "Прочее" && liveSelectedService.id !== "cc:other"
|
||||
? <Flag code={liveSelectedService.label} size={22} />
|
||||
: <ServiceBrandIcon label={destMode === "countries" ? "Прочее" : liveSelectedService.label} size={22} />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-mono font-semibold text-sm truncate">{liveSelectedService.label}</p>
|
||||
<p className="font-mono font-semibold text-sm truncate">
|
||||
{destDisplayLabel(liveSelectedService, destMode)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Конечный сервис · {liveSelectedService.category}
|
||||
{destMode === "countries"
|
||||
? `Конечная страна${liveSelectedService.label !== "Прочее" ? ` · ${liveSelectedService.label}` : ""}`
|
||||
: `Конечный сервис · ${liveSelectedService.category}`}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -2769,12 +2900,18 @@ export default function NetworkMapPage() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!mapAsnLoaded && (
|
||||
{!mapAsnLoaded && destMode === "services" && (
|
||||
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
||||
<span className="text-xs text-muted-foreground">GeoLite2 ASN</span>
|
||||
<span className="text-xs font-mono font-medium text-amber-500">не загружена</span>
|
||||
</div>
|
||||
)}
|
||||
{destMode === "countries" && !mapCountryLoaded && (
|
||||
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
||||
<span className="text-xs text-muted-foreground">GeoIP Country</span>
|
||||
<span className="text-xs font-mono font-medium text-amber-500">RIPE-кэш</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between py-2 border-b border-border/50">
|
||||
<span className="text-xs text-muted-foreground">Скорость</span>
|
||||
<span className="text-xs font-mono font-medium">
|
||||
@@ -2792,7 +2929,7 @@ export default function NetworkMapPage() {
|
||||
<div className="flex flex-col gap-3">
|
||||
{visibleServiceEdges.filter((e) => e.toId === liveSelectedService.id).map((e) => {
|
||||
const src = mapServers.find((s) => s.id === e.fromId)
|
||||
const enPaths = mapServicePaths
|
||||
const enPaths = destPaths
|
||||
.filter((p) => p.serviceId === liveSelectedService.id && p.enId === e.fromId)
|
||||
.slice()
|
||||
.sort((a, b) => b.bps - a.bps)
|
||||
@@ -2807,9 +2944,10 @@ export default function NetworkMapPage() {
|
||||
<ServicePathList
|
||||
paths={enPaths}
|
||||
servers={mapServers}
|
||||
services={mapServices}
|
||||
services={destNodes}
|
||||
highlight={highlightedPath}
|
||||
viaMode="via"
|
||||
destMode={destMode}
|
||||
onToggle={togglePathHighlight}
|
||||
/>
|
||||
</div>
|
||||
@@ -3048,7 +3186,7 @@ export default function NetworkMapPage() {
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">Пути</p>
|
||||
<ServicePathList
|
||||
paths={mapServicePaths
|
||||
paths={destPaths
|
||||
.filter((p) => (
|
||||
selected.type === "exit-node"
|
||||
? p.enId === selected.id
|
||||
@@ -3057,9 +3195,10 @@ export default function NetworkMapPage() {
|
||||
.slice()
|
||||
.sort((a, b) => b.bps - a.bps)}
|
||||
servers={mapServers}
|
||||
services={mapServices}
|
||||
services={destNodes}
|
||||
highlight={highlightedPath}
|
||||
viaMode="service"
|
||||
destMode={destMode}
|
||||
onToggle={togglePathHighlight}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user