diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9dc49f0..50ab372 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -41,6 +41,7 @@ import ToastContainer from './components/ToastContainer.jsx'; import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPalette.jsx'; import ErrorBoundary from './components/ErrorBoundary.jsx'; import NetworkErrorHandler from './components/NetworkErrorHandler.jsx'; +import { PingProvider } from './contexts/PingContext.jsx'; // --- Simple i18n (RU/EN) --- const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k }); @@ -104,12 +105,14 @@ function App() { - - - - - - + + + + + + + + diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx index d7147f4..faa2371 100644 --- a/frontend/src/EasySwitchManager.jsx +++ b/frontend/src/EasySwitchManager.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import api from './lib/api.js'; +import { usePing } from './contexts/PingContext.jsx'; import { IconSearch, IconRefresh, @@ -49,8 +50,8 @@ function EasySwitchManager() { const [hasChanges, setHasChanges] = useState(false); const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам - // Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс - const [pingMap, setPingMap] = useState({}); + // Единый источник пингов (кеш + API) — общий с страницей фильтров + const { pingMap, requestPings } = usePing(); useEffect(() => { const initData = async () => { @@ -212,8 +213,21 @@ function EasySwitchManager() { // Фильтруем серверы, у которых есть gateways const serversWithGateways = serversWithData.filter(s => s.gateways.length > 0); setServers(serversWithGateways); - // Предзагружаем пинги для всех комбинаций server+gateway - prefetchPings(serversWithGateways, inventory); + // Единая точка входа: запрашиваем пинги через контекст (кеш общий с фильтрами) + const pingTasks = []; + serversWithGateways.forEach((server) => { + const inv = inventory.find(srv => + String(srv.dns || '').trim() === server.name || + String(srv.hostName || '').trim() === server.name || + String(srv.ip || '').trim() === server.name + ); + const routerId = inv?.id || inv?.dns || inv?.ip; + if (!routerId) return; + (server.gateways || []).forEach((gw) => { + if (gw.ip) pingTasks.push({ routerId, gatewayIp: gw.ip }); + }); + }); + requestPings(pingTasks); // Отладочная информация try { @@ -254,59 +268,6 @@ function EasySwitchManager() { } }; - /** - * Загрузить реальные пинги для комбинаций (сервер, gateway). - * Цель пинга берётся из настроек интерфейса («Домен для пинга») или по умолчанию. - */ - const prefetchPings = async (serversList, inventory) => { - try { - const tasks = []; - const seen = new Set(); - - serversList.forEach((server) => { - const inv = inventory.find(srv => - String(srv.dns || '').trim() === server.name || - String(srv.hostName || '').trim() === server.name || - String(srv.ip || '').trim() === server.name - ); - const routerId = inv?.id || inv?.dns || inv?.ip; - if (!routerId) return; - - (server.gateways || []).forEach((gw) => { - const ip = gw.ip || ''; - if (!ip) return; - const key = `${routerId}:${ip}`; - if (seen.has(key) || pingMap[key] !== undefined) return; - seen.add(key); - tasks.push({ routerId, ip, key }); - }); - }); - - if (tasks.length === 0) return; - - // Запросы идут параллельно; обновляем UI по мере прихода каждого ответа, - // чтобы значения на интерфейсе отображались быстрее. - tasks.forEach(({ routerId, ip, key }) => { - api - .post('/mikrotik/ping', { - serverId: routerId, - gatewayIp: ip, - count: 5, - }) - .then(({ data }) => { - const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; - setPingMap((prev) => ({ ...prev, [key]: value })); - }) - .catch((e) => { - console.warn('Failed to fetch ping for', routerId, ip, e?.message || e); - setPingMap((prev) => ({ ...prev, [key]: null })); - }); - }); - } catch (e) { - console.warn('prefetchPings error:', e?.message || e); - } - }; - const handleGatewaySelect = (serverId, community, gateway) => { const key = `${serverId}:${community}`; setActiveGateways(prev => { diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index 79a90d7..af981b0 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -51,6 +51,7 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { normalizeGateways, countryToFlag } from './utils/serverUtils.js'; +import { usePing } from './contexts/PingContext.jsx'; import { AddFilterModal, EditFilterModal, @@ -782,8 +783,8 @@ function FilterManager() { const [inventoryServers, setInventoryServers] = useState([]); // Network config gateways (from /network-config endpoint) const [networkConfigGateways, setNetworkConfigGateways] = useState([]); - // Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс (как на Easy Switch) - const [pingMap, setPingMap] = useState({}); + // Единая точка входа для пингов (кеш общий с Easy Switch) + const { pingMap, requestPings } = usePing(); // Получить metadata сервера из inventory по имени (dns/hostName/ip) const getInventoryMetaForServer = (serverName) => { @@ -888,55 +889,17 @@ function FilterManager() { }); })(); - // Предзагружаем пинги для gateways выбранного сервера (как на Easy Switch, но только для одного сервера) - const prefetchPingsForSelectedServer = () => { - try { - if (!selectedServer) return; - const routerId = getRouterIdForServer(selectedServer); - if (!routerId) return; - if (!selectedServerGateways || selectedServerGateways.length === 0) return; - - const tasks = []; - const seen = new Set(); - - selectedServerGateways.forEach((gw) => { - const ip = gw.ip || ''; - if (!ip) return; - const key = `${routerId}:${ip}`; - if (seen.has(key) || Object.prototype.hasOwnProperty.call(pingMap, key)) return; - seen.add(key); - tasks.push({ routerId, ip, key }); - }); - - if (tasks.length === 0) return; - - tasks.forEach(({ routerId: rId, ip, key }) => { - api - .post('/mikrotik/ping', { - serverId: rId, - gatewayIp: ip, - count: 5, - }) - .then(({ data }) => { - const value = - typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; - setPingMap((prev) => ({ ...prev, [key]: value })); - }) - .catch((e) => { - console.warn('Failed to fetch ping for', rId, ip, e?.message || e); - setPingMap((prev) => ({ ...prev, [key]: null })); - }); - }); - } catch (e) { - console.warn('prefetchPingsForSelectedServer error:', e?.message || e); - } - }; - - // Автоматически обновляем пинги при смене выбранного сервера или списка его gateways + // Запрашиваем пинги для gateways выбранного сервера через единый контекст (кеш общий с Easy Switch) useEffect(() => { - prefetchPingsForSelectedServer(); - }, [selectedServer, selectedServerGateways, inventoryServers]); - + if (!selectedServer || !selectedServerGateways?.length) return; + const routerId = getRouterIdForServer(selectedServer); + if (!routerId) return; + const tasks = selectedServerGateways + .filter((gw) => gw.ip) + .map((gw) => ({ routerId, gatewayIp: gw.ip })); + requestPings(tasks); + }, [selectedServer, selectedServerGateways, requestPings]); + useEffect(() => { // Загружаем базовую AS из настроек (async () => { diff --git a/frontend/src/contexts/PingContext.jsx b/frontend/src/contexts/PingContext.jsx new file mode 100644 index 0000000..360364a --- /dev/null +++ b/frontend/src/contexts/PingContext.jsx @@ -0,0 +1,92 @@ +import { createContext, useContext, useState, useCallback, useRef } from 'react'; +import api from '../lib/api.js'; + +/** + * Единая точка входа для пингов до шлюзов (MikroTik). + * Кеш: ключ `${routerId}:${gatewayIp}` → number | null (мс или ошибка). + * Используется и на Easy Switch, и на странице фильтров (карточки gateway). + */ +const PingContext = createContext(null); + +const PING_COUNT = 5; + +export function PingProvider({ children }) { + const [pingMap, setPingMap] = useState({}); + const inFlightRef = useRef(new Set()); + const pingMapRef = useRef(pingMap); + pingMapRef.current = pingMap; + + const getKey = (routerId, gatewayIp) => { + if (!routerId || !gatewayIp) return null; + return `${routerId}:${gatewayIp}`; + }; + + const getPing = useCallback((routerId, gatewayIp) => { + const key = getKey(routerId, gatewayIp); + if (!key) return undefined; + return Object.prototype.hasOwnProperty.call(pingMap, key) ? pingMap[key] : undefined; + }, [pingMap]); + + /** + * Запросить пинги для списка пар (routerId, gatewayIp). + * Уже закешированные или запрашиваемые ключи пропускаются. Один вызов API на ключ. + * @param tasks Array<{ routerId, gatewayIp }> + */ + const requestPings = useCallback((tasks) => { + if (!Array.isArray(tasks) || tasks.length === 0) return; + const cache = pingMapRef.current; + + const toRequest = []; + tasks.forEach(({ routerId, gatewayIp }) => { + const ip = gatewayIp || ''; + if (!routerId || !ip) return; + const key = getKey(routerId, ip); + if (!key) return; + if (Object.prototype.hasOwnProperty.call(cache, key)) return; + if (inFlightRef.current.has(key)) return; + inFlightRef.current.add(key); + toRequest.push({ routerId, ip, key }); + }); + + toRequest.forEach(({ routerId, ip, key }) => { + api + .post('/mikrotik/ping', { + serverId: routerId, + gatewayIp: ip, + count: PING_COUNT, + }) + .then(({ data }) => { + const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; + setPingMap((prev) => ({ ...prev, [key]: value })); + }) + .catch((e) => { + console.warn('PingContext: failed to fetch ping for', routerId, ip, e?.message || e); + setPingMap((prev) => ({ ...prev, [key]: null })); + }) + .finally(() => { + inFlightRef.current.delete(key); + }); + }); + }, []); + + const value = { + pingMap, + getPing, + requestPings, + getKey, + }; + + return ( + + {children} + + ); +} + +export function usePing() { + const ctx = useContext(PingContext); + if (!ctx) { + throw new Error('usePing must be used within PingProvider'); + } + return ctx; +}