diff --git a/app/(main)/network-map/page.tsx b/app/(main)/network-map/page.tsx index 6334243..e489873 100644 --- a/app/(main)/network-map/page.tsx +++ b/app/(main)/network-map/page.tsx @@ -938,6 +938,7 @@ export default function NetworkMapPage() { const [mapHops, setMapHops] = useState([]) const [mapServices, setMapServices] = useState([]) const [mapServiceEdges, setMapServiceEdges] = useState([]) + const [mapSharePct, setMapSharePct] = useState(5) /** FQDN из GRE outer → IPv4 (ответ POST /api/network/resolve-hosts), для матчинга с WAN. */ const [greResolvedIpv4ByHost, setGreResolvedIpv4ByHost] = useState>({}) const [dataError, setDataError] = useState(null) @@ -1032,6 +1033,7 @@ export default function NetworkMapPage() { setMapHops([]) setMapServices(MOCK_MAP_SERVICES) setMapServiceEdges(MOCK_MAP_SERVICE_EDGES) + setMapSharePct(5) setDataError(null) }) return @@ -1106,6 +1108,7 @@ export default function NetworkMapPage() { setMapHops([]) setMapServices(MOCK_MAP_SERVICES) setMapServiceEdges(MOCK_MAP_SERVICE_EDGES) + setMapSharePct(5) }) return } @@ -1118,25 +1121,29 @@ export default function NetworkMapPage() { return } let cancelled = false + let ac: AbortController | null = null const tick = () => { - apiFetch("/api/traffic/flow/map-hops?range=5m") + ac?.abort() + ac = new AbortController() + apiFetch("/api/traffic/flow/map-hops?range=5m", { signal: ac.signal }) .then((res) => { if (cancelled) return setMapHops(res.hops ?? []) setMapServices(res.services ?? []) setMapServiceEdges(res.serviceEdges ?? []) + if (res.mapServiceMinSharePct != null) setMapSharePct(res.mapServiceMinSharePct) }) - .catch(() => { + .catch((err: unknown) => { if (cancelled) return - setMapHops([]) - setMapServices([]) - setMapServiceEdges([]) + const name = err instanceof Error ? err.name : "" + if (name === "AbortError") return }) } tick() const id = window.setInterval(tick, 4000) return () => { cancelled = true + ac?.abort() window.clearInterval(id) } }, [useLiveData, showNetflow, showServices, apiFetch]) @@ -1736,6 +1743,11 @@ export default function NetworkMapPage() { ))} +

+ {mapSharePct > 0 + ? `Порог доли сервиса ≥ ${mapSharePct}% · Настройки → NetFlow` + : "Порог доли выключен (все бренды, макс. 20) · Настройки → NetFlow"} +

{(Object.keys(nodePositions).length > 0 || Object.keys(satPositions).length > 0) && (

diff --git a/packages/contracts/src/traffic-flow.ts b/packages/contracts/src/traffic-flow.ts index 8bf8d11..ae947a6 100644 --- a/packages/contracts/src/traffic-flow.ts +++ b/packages/contracts/src/traffic-flow.ts @@ -21,6 +21,7 @@ export const trafficFlowSettingsDtoSchema = z.object({ hubServerId: z.number().int().positive().nullable(), retentionHours: z.number().int().positive(), topN: z.number().int().positive(), + mapServiceMinSharePct: z.number().min(0).max(100), lastDatagramAt: z.string().nullable(), lastExporterIp: z.string().nullable(), lastError: z.string().nullable(), @@ -40,6 +41,7 @@ export const trafficFlowSettingsPatchSchema = z.object({ hubServerId: z.number().int().positive().nullable().optional(), retentionHours: z.number().int().positive().optional(), topN: z.number().int().positive().max(1000).optional(), + mapServiceMinSharePct: z.number().min(0).max(100).optional(), }) export const trafficFlowOverlayRequestSchema = z.object({ @@ -289,6 +291,7 @@ export const flowMapHopsDtoSchema = z.object({ totalBytes: z.number().nonnegative().optional(), services: z.array(flowMapServiceDtoSchema).optional(), serviceEdges: z.array(flowMapServiceEdgeDtoSchema).optional(), + mapServiceMinSharePct: z.number().min(0).max(100).optional(), dedupApplied: z.boolean(), excludeMeshApplied: z.boolean(), excludeOverlayApplied: z.boolean(),