import { useState, useEffect, useCallback, useRef } from 'react'; import api from './lib/api.js'; import { IconRefresh, IconTopologyRing } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; import NetworkMapUnifi from './NetworkMapUnifi.jsx'; /** Параллельно выполняем промисы с лимитом одновременных */ async function runWithLimit(tasks, limit = 4) { const results = []; let index = 0; async function runNext() { const i = index++; if (i >= tasks.length) return; const task = tasks[i]; try { const value = await task(); results[i] = { value }; } catch (err) { results[i] = { error: err }; } await runNext(); } const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => runNext()); await Promise.all(workers); return results; } export default function NetworkMapDashboard() { const [servers, setServers] = useState([]); const [connections, setConnections] = useState([]); const [pingMap, setPingMap] = useState({}); const pingTimestampsRef = useRef({}); const [pingCacheSeconds, setPingCacheSeconds] = useState(0); const [loading, setLoading] = useState(true); const [pingLoading, setPingLoading] = useState(false); const [error, setError] = useState(null); const pingAbortRef = useRef(false); /** Связи строятся из интерфейсов раздела «Сетевые настройки» (/network-config), tunnelInterfaces */ const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const [serversRes, configRes, uiSettingsRes] = await Promise.all([ api.get('/servers'), api.get('/network-config').catch(() => ({ data: null })), api.get('/ui-settings').catch(() => ({ data: {} })), ]); const serversList = Array.isArray(serversRes?.data) ? serversRes.data : []; const config = configRes?.data || {}; const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : []; const uiSettings = uiSettingsRes?.data || {}; const ttlSeconds = Math.max( 0, parseInt(uiSettings.networkMapPingCacheSeconds, 10) || 0 ); setPingCacheSeconds(ttlSeconds); const getServer = (serverId) => serversList.find((s) => s.id === serverId || s.ip === serverId || s.dns === serverId); const connList = []; tunnelInterfaces.forEach((iface) => { if (!iface.serverId || !iface.serverId2) return; const s1 = getServer(iface.serverId); const s2 = getServer(iface.serverId2); if (!s1 || !s2 || s1.ip === s2.ip) return; const s1Key = s1.id || s1.dns || s1.ip; const s2Key = s2.id || s2.dns || s2.ip; if (!s1Key || !s2Key) return; connList.push({ from: s1.ip, to: s2.ip, tunnelType: iface.type || 'GRE', // Внутренние IP туннеля: с сервера 1 до сервера 2 и обратно fromKey: s1Key, toKey: s2Key, internalFromTo: iface.remoteIp || null, // ping: server1 -> remoteIp internalToFrom: iface.localIp || null, // ping: server2 -> localIp }); }); setServers(serversList); setConnections(connList); } catch (e) { console.error('NetworkMap fetch:', e); setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить данные'); setServers([]); setConnections([]); } finally { setLoading(false); } }, []); const requestPings = useCallback(async () => { if (connections.length === 0) return; pingAbortRef.current = false; setPingLoading(true); const key = (a, b) => `${a}:${b}`; const ttlMs = Math.max(0, (pingCacheSeconds || 0) * 1000); const tasks = []; connections.forEach((c) => { const from = String(c.from); const to = String(c.to); if (!from || !to || from === to) return; const srcFrom = servers.find((s) => s.ip === from); const srcTo = servers.find((s) => s.ip === to); const canPingFrom = srcFrom && ['jumphost', 'home'].includes(String(srcFrom.type || '').toLowerCase()) && c.internalFromTo; const canPingTo = srcTo && ['jumphost', 'home'].includes(String(srcTo.type || '').toLowerCase()) && c.internalToFrom; // Проверка кеша: если TTL не истёк, не запускаем новый запрос const keyFromTo = key(from, to); const keyToFrom = key(to, from); const now = Date.now(); if (ttlMs > 0) { const tsFrom = pingTimestampsRef.current[keyFromTo]; if (tsFrom && now - tsFrom < ttlMs) { // уже есть актуальное значение для from->to } else if (!canPingFrom) { // если пинговать нельзя, но кеша нет — оставляем как есть } const tsTo = pingTimestampsRef.current[keyToFrom]; if (tsTo && now - tsTo < ttlMs) { // уже есть актуальное значение для to->from } else if (!canPingTo) { // нельзя пинговать и кеша нет — пропускаем } } // Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config if (canPingFrom) { tasks.push(async () => { if (pingAbortRef.current) return; try { const { data } = await api.post('/mikrotik/ping', { serverId: c.fromKey, target: c.internalFromTo, gatewayIp: c.internalFromTo, count: 3, }); const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; pingTimestampsRef.current[keyFromTo] = Date.now(); setPingMap((prev) => ({ ...prev, [keyFromTo]: ms })); } catch { pingTimestampsRef.current[keyFromTo] = Date.now(); setPingMap((prev) => ({ ...prev, [keyFromTo]: null })); } }); } if (canPingTo) { tasks.push(async () => { if (pingAbortRef.current) return; try { const { data } = await api.post('/mikrotik/ping', { serverId: c.toKey, target: c.internalToFrom, gatewayIp: c.internalToFrom, count: 3, }); const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; pingTimestampsRef.current[keyToFrom] = Date.now(); setPingMap((prev) => ({ ...prev, [keyToFrom]: ms })); } catch { pingTimestampsRef.current[keyToFrom] = Date.now(); setPingMap((prev) => ({ ...prev, [keyToFrom]: null })); } }); } }); await runWithLimit(tasks, 4); if (pingAbortRef.current) return; setPingLoading(false); }, [connections, servers, pingCacheSeconds]); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { if (!loading && servers.length > 0 && connections.length > 0) { requestPings(); } return () => { pingAbortRef.current = true; }; }, [loading, connections.length]); const handleRefreshPings = () => { requestPings(); }; if (loading) { return (
} meta="Серверы и пинг между ними" />
Загрузка серверов и связей…
); } if (error) { return (
} meta="Серверы и пинг между ними" />
{error}
); } return (
} meta="Связи из раздела «Сетевые настройки» → Интерфейсы; на рёбрах — пинг (мс)" actions={ } /> {servers.length === 0 ? (
Нет серверов. Добавьте серверы в разделе «Серверы».
) : ( )}
); }