diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index 1eccb70..91fedc3 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import api from './lib/api.js'; import { useNotify } from './components/NotifyProvider.jsx'; import FormModal from './components/FormModal.jsx'; @@ -291,6 +291,18 @@ function moveInArray(arr, fromIndex, toIndex) { return copy; } +function getOspfRouterIdentity(server) { + if (!server || typeof server !== 'object') { + return { key: 'unknown', label: 'Unknown router' }; + } + const host = String(server.mikrotikHost || server.ip || server.dns || server.id || '').trim(); + const port = String(server.mikrotikPort || 80).trim(); + const user = String(server.mikrotikUser || 'admin').trim(); + const key = `${host}::${port}::${user}`; + const label = host || String(server.dns || server.ip || server.id || 'Unknown router'); + return { key, label }; +} + // Пустые объекты const getEmptyGateway = () => ({ id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, @@ -392,6 +404,7 @@ function NetworkConfigManager() { const [ospfDraggingItem, setOspfDraggingItem] = useState(null); const [ospfLoading, setOspfLoading] = useState(false); const [ospfSaving, setOspfSaving] = useState(false); + const ospfAutoLoadedRef = useRef(false); // === IPSec Passwords Management === const [ipsecPasswords, setIpsecPasswords] = useState([]); @@ -669,28 +682,45 @@ function NetworkConfigManager() { }); }, [homeOspfCandidatesByServer]); - const ospfTemplatesByHomeServer = useMemo(() => { + const ospfTemplatesByRouter = useMemo(() => { const grouped = {}; (config.ospfInterfaceTemplates || []).forEach((item) => { const sid = String(item?.serverId || ''); if (!sid) return; - if (!grouped[sid]) grouped[sid] = []; - grouped[sid].push(item); + const server = getServerInfo(sid); + const identity = getOspfRouterIdentity(server || { id: sid, ip: sid }); + if (!grouped[identity.key]) { + grouped[identity.key] = { + routerKey: identity.key, + routerLabel: identity.label, + serverIds: new Set(), + representativeServerId: sid, + items: [], + }; + } + grouped[identity.key].serverIds.add(sid); + grouped[identity.key].items.push(item); }); - Object.keys(grouped).forEach((sid) => { - grouped[sid].sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0)); + + const groups = Object.values(grouped).map((group) => { + const sorted = [...group.items].sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0)); + return { + ...group, + serverIds: Array.from(group.serverIds), + items: sorted, + }; }); - return grouped; - }, [config.ospfInterfaceTemplates]); - const ospfHomeServerIds = useMemo(() => { - let ids = Object.keys(homeOspfCandidatesByServer); - if (serverFilter) ids = ids.filter((sid) => String(sid) === String(serverFilter)); - return ids; - }, [homeOspfCandidatesByServer, serverFilter]); + if (serverFilter) { + const filterId = String(serverFilter); + return groups.filter((group) => group.serverIds.includes(filterId)); + } - const handleDropOspfTemplate = (serverId, targetKey) => { - if (!ospfDraggingItem || String(ospfDraggingItem.serverId) !== String(serverId)) return; + return groups; + }, [config.ospfInterfaceTemplates, serverFilter, servers]); + + const handleDropOspfTemplate = (routerKey, targetKey) => { + if (!ospfDraggingItem || String(ospfDraggingItem.routerKey) !== String(routerKey)) return; const draggedKey = String(ospfDraggingItem.key || ''); const destinationKey = String(targetKey || ''); if (!draggedKey || !destinationKey || draggedKey === destinationKey) { @@ -700,7 +730,12 @@ function NetworkConfigManager() { setConfig((prev) => { const current = (prev.ospfInterfaceTemplates || []) - .filter((item) => String(item.serverId || '') === String(serverId)) + .filter((item) => { + const sid = String(item.serverId || ''); + const server = getServerInfo(sid); + const identity = getOspfRouterIdentity(server || { id: sid, ip: sid }); + return identity.key === String(routerKey); + }) .sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0)); const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey); const toIndex = current.findIndex((item) => String(item.key || '') === destinationKey); @@ -711,9 +746,12 @@ function NetworkConfigManager() { cost: (arr.length - idx) * OSPF_COST_STEP, })); - const otherServers = (prev.ospfInterfaceTemplates || []).filter( - (item) => String(item.serverId || '') !== String(serverId) - ); + const otherServers = (prev.ospfInterfaceTemplates || []).filter((item) => { + const sid = String(item.serverId || ''); + const server = getServerInfo(sid); + const identity = getOspfRouterIdentity(server || { id: sid, ip: sid }); + return identity.key !== String(routerKey); + }); return { ...prev, @@ -724,7 +762,7 @@ function NetworkConfigManager() { setOspfDraggingItem(null); }; - const handleLoadOspfFromMikrotik = async () => { + const handleLoadOspfFromMikrotik = async ({ silent = false } = {}) => { setOspfLoading(true); try { const response = await api.get('/mikrotik/ospf-interface-templates'); @@ -736,25 +774,26 @@ function NetworkConfigManager() { ospfInterfaceTemplates: mergeOspfCostsFromRouter(prev.ospfInterfaceTemplates || [], results), })); - if (results.length > 0) { + if (!silent && results.length > 0) { notify.success(`OSPF загружен с HOME роутеров: ${results.length}`); - } else { + } else if (!silent) { notify.warning('Не удалось получить OSPF шаблоны ни с одного HOME роутера'); } - if (errors.length > 0) { + if (!silent && errors.length > 0) { notify.warning(`Ошибки чтения OSPF: ${errors.length} роутер(ов)`); } } catch (error) { console.error('Error loading OSPF templates:', error); - notify.error('Не удалось загрузить OSPF Interface Template с MikroTik'); + if (!silent) { + notify.error('Не удалось загрузить OSPF Interface Template с MikroTik'); + } } finally { setOspfLoading(false); } }; const handleSaveOspfToMikrotik = async () => { - const serverEntries = Object.entries(ospfTemplatesByHomeServer); - if (serverEntries.length === 0) { + if (ospfTemplatesByRouter.length === 0) { notify.warning('Нет OSPF шаблонов для сохранения'); return; } @@ -762,13 +801,23 @@ function NetworkConfigManager() { setOspfSaving(true); try { const payload = { - servers: serverEntries.map(([serverId, templates]) => ({ - serverId, - templates: templates.map((item) => ({ - interfaceName: item.interfaceName, - cost: item.cost, - })), - })), + servers: ospfTemplatesByRouter.map((group) => { + const dedup = new Map(); + group.items.forEach((item) => { + const key = String(item.interfaceName || '').trim().toUpperCase(); + if (!key) return; + if (!dedup.has(key)) { + dedup.set(key, { + interfaceName: item.interfaceName, + cost: item.cost, + }); + } + }); + return { + serverId: group.representativeServerId, + templates: Array.from(dedup.values()), + }; + }), }; const response = await api.post('/mikrotik/ospf-interface-templates/apply', payload); @@ -791,6 +840,13 @@ function NetworkConfigManager() { } }; + useEffect(() => { + if (activeTab !== 'interfaces') return; + if (ospfAutoLoadedRef.current) return; + ospfAutoLoadedRef.current = true; + handleLoadOspfFromMikrotik({ silent: true }); + }, [activeTab]); + // === Группировка по серверам === const gatewaysByServer = useMemo(() => { const grouped = {}; @@ -4010,20 +4066,19 @@ function NetworkConfigManager() {