feat(EasySwitchManager, FilterManager): implement location-based ping requests to refresh data on route changes
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m56s

This commit is contained in:
2026-02-12 13:34:03 +07:00
parent 219ca7450c
commit c75f5f6b82
3 changed files with 44 additions and 7 deletions
+21
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import api from './lib/api.js'; import api from './lib/api.js';
import { usePing } from './contexts/PingContext.jsx'; import { usePing } from './contexts/PingContext.jsx';
import { import {
@@ -52,6 +53,7 @@ function EasySwitchManager() {
const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам
// Единый источник пингов (кеш + API) — общий с страницей фильтров // Единый источник пингов (кеш + API) — общий с страницей фильтров
const { pingMap, requestPings } = usePing(); const { pingMap, requestPings } = usePing();
const location = useLocation();
useEffect(() => { useEffect(() => {
const initData = async () => { const initData = async () => {
@@ -89,6 +91,25 @@ function EasySwitchManager() {
initData(); initData();
}, []); }, []);
// При переходе на раздел Easy Switch перезапрашиваем пинги (устаревшие по TTL обновятся)
useEffect(() => {
if (location.pathname !== '/easy-switch' || !servers.length || !inventoryServers.length) return;
const pingTasks = [];
servers.forEach((server) => {
const inv = inventoryServers.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);
}, [location.pathname, servers, inventoryServers, requestPings]);
const loadDataWithParams = async (communities, inventory, ncGatewaysParam) => { const loadDataWithParams = async (communities, inventory, ncGatewaysParam) => {
setLoading(true); setLoading(true);
setError(''); setError('');
+6 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import api from './lib/api.js'; import api from './lib/api.js';
import ConfirmDialog from './components/ConfirmDialog.jsx'; import ConfirmDialog from './components/ConfirmDialog.jsx';
import FormModal from './components/FormModal.jsx'; import FormModal from './components/FormModal.jsx';
@@ -895,16 +895,19 @@ function FilterManager() {
}); });
})(); })();
// Запрашиваем пинги для gateways выбранного сервера через единый контекст (кеш общий с Easy Switch) const location = useLocation();
// Запрашиваем пинги для gateways выбранного сервера; при переходе на раздел Фильтров обновляем (устаревшие по TTL перезапросятся)
useEffect(() => { useEffect(() => {
if (!selectedServer || !selectedServerGateways?.length) return; if (!selectedServer || !selectedServerGateways?.length) return;
if (location.pathname !== '/filters') return;
const routerId = getRouterIdForServer(selectedServer); const routerId = getRouterIdForServer(selectedServer);
if (!routerId) return; if (!routerId) return;
const tasks = selectedServerGateways const tasks = selectedServerGateways
.filter((gw) => gw.ip) .filter((gw) => gw.ip)
.map((gw) => ({ routerId, gatewayIp: gw.ip })); .map((gw) => ({ routerId, gatewayIp: gw.ip }));
requestPings(tasks); requestPings(tasks);
}, [selectedServer, selectedServerGateways, requestPings]); }, [location.pathname, selectedServer, selectedServerGateways, requestPings]);
useEffect(() => { useEffect(() => {
// Загружаем базовую AS из настроек // Загружаем базовую AS из настроек
+17 -4
View File
@@ -4,16 +4,20 @@ import api from '../lib/api.js';
/** /**
* Единая точка входа для пингов до шлюзов (MikroTik). * Единая точка входа для пингов до шлюзов (MikroTik).
* Кеш: ключ `${routerId}:${gatewayIp}` → number | null (мс или ошибка). * Кеш: ключ `${routerId}:${gatewayIp}` → number | null (мс или ошибка).
* TTL: после истечения записи перезапрашиваются при переходе на раздел.
* Используется и на Easy Switch, и на странице фильтров (карточки gateway). * Используется и на Easy Switch, и на странице фильтров (карточки gateway).
*/ */
const PingContext = createContext(null); const PingContext = createContext(null);
const PING_COUNT = 5; const PING_COUNT = 5;
/** Время жизни кеша пинга (мс). После истечения при открытии раздела данные перезапрашиваются. */
const PING_CACHE_TTL_MS = 2 * 60 * 1000; // 2 минуты
export function PingProvider({ children }) { export function PingProvider({ children }) {
const [pingMap, setPingMap] = useState({}); const [pingMap, setPingMap] = useState({});
const inFlightRef = useRef(new Set()); const inFlightRef = useRef(new Set());
const pingMapRef = useRef(pingMap); const pingMapRef = useRef(pingMap);
const timestampsRef = useRef({}); // key → Date.now() когда запись установлена
pingMapRef.current = pingMap; pingMapRef.current = pingMap;
const getKey = (routerId, gatewayIp) => { const getKey = (routerId, gatewayIp) => {
@@ -21,6 +25,14 @@ export function PingProvider({ children }) {
return `${routerId}:${gatewayIp}`; return `${routerId}:${gatewayIp}`;
}; };
const isCacheValid = useCallback((key) => {
const cache = pingMapRef.current;
if (!Object.prototype.hasOwnProperty.call(cache, key)) return false;
const ts = timestampsRef.current[key];
if (ts == null) return false;
return (Date.now() - ts) < PING_CACHE_TTL_MS;
}, []);
const getPing = useCallback((routerId, gatewayIp) => { const getPing = useCallback((routerId, gatewayIp) => {
const key = getKey(routerId, gatewayIp); const key = getKey(routerId, gatewayIp);
if (!key) return undefined; if (!key) return undefined;
@@ -29,12 +41,11 @@ export function PingProvider({ children }) {
/** /**
* Запросить пинги для списка пар (routerId, gatewayIp). * Запросить пинги для списка пар (routerId, gatewayIp).
* Уже закешированные или запрашиваемые ключи пропускаются. Один вызов API на ключ. * Пропускаются ключи с валидным кешем (не истёк TTL) и уже запрашиваемые.
* @param tasks Array<{ routerId, gatewayIp }> * @param tasks Array<{ routerId, gatewayIp }>
*/ */
const requestPings = useCallback((tasks) => { const requestPings = useCallback((tasks) => {
if (!Array.isArray(tasks) || tasks.length === 0) return; if (!Array.isArray(tasks) || tasks.length === 0) return;
const cache = pingMapRef.current;
const toRequest = []; const toRequest = [];
tasks.forEach(({ routerId, gatewayIp }) => { tasks.forEach(({ routerId, gatewayIp }) => {
@@ -42,7 +53,7 @@ export function PingProvider({ children }) {
if (!routerId || !ip) return; if (!routerId || !ip) return;
const key = getKey(routerId, ip); const key = getKey(routerId, ip);
if (!key) return; if (!key) return;
if (Object.prototype.hasOwnProperty.call(cache, key)) return; if (isCacheValid(key)) return;
if (inFlightRef.current.has(key)) return; if (inFlightRef.current.has(key)) return;
inFlightRef.current.add(key); inFlightRef.current.add(key);
toRequest.push({ routerId, ip, key }); toRequest.push({ routerId, ip, key });
@@ -57,17 +68,19 @@ export function PingProvider({ children }) {
}) })
.then(({ data }) => { .then(({ data }) => {
const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
timestampsRef.current[key] = Date.now();
setPingMap((prev) => ({ ...prev, [key]: value })); setPingMap((prev) => ({ ...prev, [key]: value }));
}) })
.catch((e) => { .catch((e) => {
console.warn('PingContext: failed to fetch ping for', routerId, ip, e?.message || e); console.warn('PingContext: failed to fetch ping for', routerId, ip, e?.message || e);
timestampsRef.current[key] = Date.now();
setPingMap((prev) => ({ ...prev, [key]: null })); setPingMap((prev) => ({ ...prev, [key]: null }));
}) })
.finally(() => { .finally(() => {
inFlightRef.current.delete(key); inFlightRef.current.delete(key);
}); });
}); });
}, []); }, [isCacheValid]);
const value = { const value = {
pingMap, pingMap,