Files
router-lists-ui/frontend/src/NetworkMapDashboard.jsx
T
2026-02-17 17:08:57 +07:00

255 lines
8.9 KiB
React

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 (
<div>
<PageHeader
title="Карта сети"
icon={<IconTopologyRing size={24} />}
meta="Серверы и пинг между ними"
/>
<div className="text-center py-5 text-muted">Загрузка серверов и связей</div>
</div>
);
}
if (error) {
return (
<div>
<PageHeader
title="Карта сети"
icon={<IconTopologyRing size={24} />}
meta="Серверы и пинг между ними"
/>
<div className="alert alert-danger">{error}</div>
</div>
);
}
return (
<div>
<PageHeader
title="Карта сети"
icon={<IconTopologyRing size={24} />}
meta="Связи из раздела «Сетевые настройки» → Интерфейсы; на рёбрах — пинг (мс)"
actions={
<button
type="button"
className="btn btn-outline-primary"
onClick={handleRefreshPings}
disabled={pingLoading}
>
<IconRefresh className={pingLoading ? 'spin me-2' : 'me-2'} size={18} />
{pingLoading ? 'Пинг…' : 'Обновить пинг'}
</button>
}
/>
{servers.length === 0 ? (
<div className="alert alert-info">
Нет серверов. Добавьте серверы в разделе «Серверы».
</div>
) : (
<NetworkMapUnifi
servers={servers}
connections={connections}
pingMap={pingMap}
/>
)}
</div>
);
}