feat(PingContext): integrate PingProvider for centralized ping management across components
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m5s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m5s
This commit is contained in:
@@ -41,6 +41,7 @@ import ToastContainer from './components/ToastContainer.jsx';
|
|||||||
import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPalette.jsx';
|
import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPalette.jsx';
|
||||||
import ErrorBoundary from './components/ErrorBoundary.jsx';
|
import ErrorBoundary from './components/ErrorBoundary.jsx';
|
||||||
import NetworkErrorHandler from './components/NetworkErrorHandler.jsx';
|
import NetworkErrorHandler from './components/NetworkErrorHandler.jsx';
|
||||||
|
import { PingProvider } from './contexts/PingContext.jsx';
|
||||||
|
|
||||||
// --- Simple i18n (RU/EN) ---
|
// --- Simple i18n (RU/EN) ---
|
||||||
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
|
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
|
||||||
@@ -104,12 +105,14 @@ function App() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ToastContainer>
|
<PingProvider>
|
||||||
<NotifyProvider>
|
<ToastContainer>
|
||||||
<NetworkErrorHandler />
|
<NotifyProvider>
|
||||||
<MainLayout />
|
<NetworkErrorHandler />
|
||||||
</NotifyProvider>
|
<MainLayout />
|
||||||
</ToastContainer>
|
</NotifyProvider>
|
||||||
|
</ToastContainer>
|
||||||
|
</PingProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import api from './lib/api.js';
|
import api from './lib/api.js';
|
||||||
|
import { usePing } from './contexts/PingContext.jsx';
|
||||||
import {
|
import {
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
@@ -49,8 +50,8 @@ function EasySwitchManager() {
|
|||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы
|
const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы
|
||||||
const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам
|
const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам
|
||||||
// Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс
|
// Единый источник пингов (кеш + API) — общий с страницей фильтров
|
||||||
const [pingMap, setPingMap] = useState({});
|
const { pingMap, requestPings } = usePing();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
@@ -212,8 +213,21 @@ function EasySwitchManager() {
|
|||||||
// Фильтруем серверы, у которых есть gateways
|
// Фильтруем серверы, у которых есть gateways
|
||||||
const serversWithGateways = serversWithData.filter(s => s.gateways.length > 0);
|
const serversWithGateways = serversWithData.filter(s => s.gateways.length > 0);
|
||||||
setServers(serversWithGateways);
|
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 {
|
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 handleGatewaySelect = (serverId, community, gateway) => {
|
||||||
const key = `${serverId}:${community}`;
|
const key = `${serverId}:${community}`;
|
||||||
setActiveGateways(prev => {
|
setActiveGateways(prev => {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { normalizeGateways, countryToFlag } from './utils/serverUtils.js';
|
import { normalizeGateways, countryToFlag } from './utils/serverUtils.js';
|
||||||
|
import { usePing } from './contexts/PingContext.jsx';
|
||||||
import {
|
import {
|
||||||
AddFilterModal,
|
AddFilterModal,
|
||||||
EditFilterModal,
|
EditFilterModal,
|
||||||
@@ -782,8 +783,8 @@ function FilterManager() {
|
|||||||
const [inventoryServers, setInventoryServers] = useState([]);
|
const [inventoryServers, setInventoryServers] = useState([]);
|
||||||
// Network config gateways (from /network-config endpoint)
|
// Network config gateways (from /network-config endpoint)
|
||||||
const [networkConfigGateways, setNetworkConfigGateways] = useState([]);
|
const [networkConfigGateways, setNetworkConfigGateways] = useState([]);
|
||||||
// Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс (как на Easy Switch)
|
// Единая точка входа для пингов (кеш общий с Easy Switch)
|
||||||
const [pingMap, setPingMap] = useState({});
|
const { pingMap, requestPings } = usePing();
|
||||||
|
|
||||||
// Получить metadata сервера из inventory по имени (dns/hostName/ip)
|
// Получить metadata сервера из inventory по имени (dns/hostName/ip)
|
||||||
const getInventoryMetaForServer = (serverName) => {
|
const getInventoryMetaForServer = (serverName) => {
|
||||||
@@ -888,54 +889,16 @@ function FilterManager() {
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Предзагружаем пинги для gateways выбранного сервера (как на Easy Switch, но только для одного сервера)
|
// Запрашиваем пинги для 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
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
prefetchPingsForSelectedServer();
|
if (!selectedServer || !selectedServerGateways?.length) return;
|
||||||
}, [selectedServer, selectedServerGateways, inventoryServers]);
|
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(() => {
|
useEffect(() => {
|
||||||
// Загружаем базовую AS из настроек
|
// Загружаем базовую AS из настроек
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<PingContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</PingContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePing() {
|
||||||
|
const ctx = useContext(PingContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('usePing must be used within PingProvider');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user