From 99f8c5d0bf2adb44e1745ed7fb9670b30b2569ec Mon Sep 17 00:00:00 2001 From: shats Date: Thu, 5 Mar 2026 16:38:35 +0700 Subject: [PATCH] feat(app): add OSPF Tools page and integrate it into the main layout and language support --- frontend/src/App.jsx | 8 +- frontend/src/NetworkConfigManager.jsx | 379 +------------------------- frontend/src/OspfToolsPage.jsx | 307 +++++++++++++++++++++ 3 files changed, 320 insertions(+), 374 deletions(-) create mode 100644 frontend/src/OspfToolsPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 30b3f24..4f029d7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -53,6 +53,7 @@ import FirewallPage from './FirewallPage.jsx'; import SettingsPage from './SettingsPage.jsx'; import ResourceStatsPage from './ResourceStatsPage.jsx'; import RouteOptimizerPage from './RouteOptimizerPage.jsx'; +import OspfToolsPage from './OspfToolsPage.jsx'; import './App.css'; import { NotifyProvider } from './components/NotifyProvider.jsx'; import ToastContainer from './components/ToastContainer.jsx'; @@ -75,7 +76,7 @@ function LanguageProvider({ children }) { dashboard: 'Панель', trafficTraffic: 'Расход трафика', resourceStats: 'Статистика ресурсов', networkMap: 'Карта сети', uptimeMonitor: 'Uptime Monitor', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS', routeOptimizer: 'Оптимизация маршрутов ИИ', communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL', - easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', firewall: 'Firewall', + easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', firewall: 'Firewall', ospfTools: 'Инструменты OSPF', light: 'Светлая', dark: 'Тёмная', layoutSidebar: 'Сайдбар', layoutHorizontal: 'Верхнее меню' }, @@ -84,7 +85,7 @@ function LanguageProvider({ children }) { dashboard: 'Dashboard', trafficTraffic: 'Traffic Usage', resourceStats: 'Resource Stats', networkMap: 'Network Map', uptimeMonitor: 'Uptime Monitor', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs', routeOptimizer: 'AI Route Optimizer', communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs', - easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', firewall: 'Firewall', + easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', firewall: 'Firewall', ospfTools: 'OSPF Tools', light: 'Light', dark: 'Dark', layoutSidebar: 'Sidebar', layoutHorizontal: 'Top menu' } @@ -289,6 +290,7 @@ function MainLayout() { items: [ { id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload }, { id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase }, + { id: 'ospf-tools', title: t('ospfTools'), path: '/ospf-tools', icon: IconRoute2 }, { id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork }, { id: 'firewall', title: t('firewall'), path: '/firewall', icon: IconShield }, { id: 'interface-speed', title: 'Скорость интерфейсов', path: '/interface-speed', icon: IconNetwork }, @@ -466,6 +468,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> } /> } /> @@ -643,6 +646,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index dd396df..5dc500b 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import api from './lib/api.js'; import { useNotify } from './components/NotifyProvider.jsx'; import FormModal from './components/FormModal.jsx'; @@ -125,78 +125,6 @@ const GATEWAY_TEMPLATES = [ }, ]; -const OSPF_COST_STEP = 10; - -function isUnknownOspfInterfaceName(name) { - const value = String(name || '').trim(); - if (!value) return true; - if (value.startsWith('*')) return true; // служебные ссылки RouterOS вида *50 - if (value.toLowerCase() === 'unknown') return true; - return false; -} - -function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) { - const serverById = new Map((servers || []).map((s) => [String(s.id || s.ip || s.dns || ''), s])); - const byComposite = new Map(); - - (routerResults || []).forEach((result) => { - const serverId = String(result?.serverId || '').trim(); - if (!serverId) return; - const server = serverById.get(serverId) || { - id: serverId, - ip: String(result?.serverLabel || serverId), - dns: String(result?.serverLabel || ''), - }; - const identity = getOspfRouterIdentity(server); - const templates = Array.isArray(result?.templates) ? result.templates : []; - - templates.forEach((tpl) => { - const interfaceName = String(tpl?.interfaceName || '').trim(); - if (isUnknownOspfInterfaceName(interfaceName)) return; - const area = String(tpl?.area || '').trim() || 'backbone'; - const numericCost = Number(tpl?.cost); - const cost = Number.isFinite(numericCost) && numericCost >= 0 ? numericCost : OSPF_COST_STEP; - - const compositeKey = `${identity.key}::${area.toLowerCase()}::${interfaceName.toUpperCase()}`; - if (byComposite.has(compositeKey)) return; - - byComposite.set(compositeKey, { - key: compositeKey, - rosId: tpl?.id || null, - serverId, - routerKey: identity.key, - routerLabel: identity.label, - interfaceName, - area, - cost, - active: !Boolean(tpl?.disabled), - }); - }); - }); - - return Array.from(byComposite.values()); -} - -function moveInArray(arr, fromIndex, toIndex) { - if (fromIndex < 0 || toIndex < 0 || fromIndex >= arr.length || toIndex >= arr.length) return arr; - const copy = [...arr]; - const [moved] = copy.splice(fromIndex, 1); - copy.splice(toIndex, 0, moved); - 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)}`, @@ -235,7 +163,6 @@ const getDefaultConfig = () => ({ gateways: [], tunnelInterfaces: [], ipPools: [], - ospfInterfaceTemplates: [], }); function NetworkConfigManager() { @@ -295,10 +222,6 @@ function NetworkConfigManager() { const [templateMtu, setTemplateMtu] = useState(''); // MTU для шаблона const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания const [templateServerPairs, setTemplateServerPairs] = useState([]); // Массив пар серверов для множественного создания - 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([]); @@ -362,7 +285,6 @@ function NetworkConfigManager() { gateways: Array.isArray(migratedGateways) ? migratedGateways : [], tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], - ospfInterfaceTemplates: Array.isArray(data.ospfInterfaceTemplates) ? data.ospfInterfaceTemplates : [], }); } catch (error) { if (error.response?.status !== 404) { @@ -559,195 +481,6 @@ function NetworkConfigManager() { return result; }, [config.ipPools, serverFilter, searchTerm, servers]); - const ospfTemplatesByRouter = useMemo(() => { - const grouped = {}; - (config.ospfInterfaceTemplates || []).forEach((item) => { - const sid = String(item?.serverId || ''); - if (!sid) return; - 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); - }); - - const groups = Object.values(grouped).map((group) => { - const areasMap = {}; - group.items.forEach((item) => { - const area = String(item?.area || '').trim() || 'backbone'; - if (!areasMap[area]) areasMap[area] = []; - areasMap[area].push(item); - }); - - const areas = Object.entries(areasMap) - .map(([area, items]) => ({ - area, - items: [...items].sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0)), - })) - .sort((a, b) => a.area.localeCompare(b.area)); - - return { - ...group, - serverIds: Array.from(group.serverIds), - areas, - }; - }); - - if (serverFilter) { - const filterId = String(serverFilter); - return groups.filter((group) => group.serverIds.includes(filterId)); - } - - return groups; - }, [config.ospfInterfaceTemplates, serverFilter, servers]); - - const handleDropOspfTemplate = (routerKey, area, targetKey) => { - if (!ospfDraggingItem || String(ospfDraggingItem.routerKey) !== String(routerKey)) return; - if (String(ospfDraggingItem.area || '') !== String(area || '')) return; - const draggedKey = String(ospfDraggingItem.key || ''); - const destinationKey = String(targetKey || ''); - if (!draggedKey || !destinationKey || draggedKey === destinationKey) { - setOspfDraggingItem(null); - return; - } - - setConfig((prev) => { - const current = (prev.ospfInterfaceTemplates || []) - .filter((item) => { - const sid = String(item.serverId || ''); - const server = getServerInfo(sid); - const identity = getOspfRouterIdentity(server || { id: sid, ip: sid }); - const itemArea = String(item?.area || '').trim() || 'backbone'; - return identity.key === String(routerKey) && itemArea === String(area || 'backbone'); - }) - .sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0)); - const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey); - const toIndex = current.findIndex((item) => String(item.key || '') === destinationKey); - if (fromIndex < 0 || toIndex < 0) return prev; - - const reordered = moveInArray(current, fromIndex, toIndex).map((item, idx) => ({ - ...item, - cost: (idx + 1) * OSPF_COST_STEP, - })); - - const otherServers = (prev.ospfInterfaceTemplates || []).filter((item) => { - const sid = String(item.serverId || ''); - const server = getServerInfo(sid); - const identity = getOspfRouterIdentity(server || { id: sid, ip: sid }); - const itemArea = String(item?.area || '').trim() || 'backbone'; - return identity.key !== String(routerKey) || itemArea !== String(area || 'backbone'); - }); - - return { - ...prev, - ospfInterfaceTemplates: [...otherServers, ...reordered], - }; - }); - - setOspfDraggingItem(null); - }; - - const handleLoadOspfFromMikrotik = async ({ silent = false } = {}) => { - setOspfLoading(true); - try { - const response = await api.get('/mikrotik/ospf-interface-templates'); - const results = Array.isArray(response.data?.results) ? response.data.results : []; - const errors = Array.isArray(response.data?.errors) ? response.data.errors : []; - const loadedTemplates = buildOspfTemplatesFromRouterResults(results, servers); - - setConfig((prev) => ({ - ...prev, - ospfInterfaceTemplates: loadedTemplates, - })); - - if (!silent && results.length > 0) { - notify.success(`OSPF загружен с HOME роутеров: ${results.length}`); - } else if (!silent) { - notify.warning('Не удалось получить OSPF шаблоны ни с одного HOME роутера'); - } - if (!silent && errors.length > 0) { - notify.warning(`Ошибки чтения OSPF: ${errors.length} роутер(ов)`); - } - } catch (error) { - console.error('Error loading OSPF templates:', error); - if (!silent) { - notify.error('Не удалось загрузить OSPF Interface Template с MikroTik'); - } - } finally { - setOspfLoading(false); - } - }; - - const handleSaveOspfToMikrotik = async () => { - if (ospfTemplatesByRouter.length === 0) { - notify.warning('Нет OSPF шаблонов для сохранения'); - return; - } - - setOspfSaving(true); - try { - const payload = { - servers: ospfTemplatesByRouter.map((group) => { - const dedup = new Map(); - (group.areas || []).forEach((areaGroup) => { - (areaGroup.items || []).forEach((item) => { - const interfaceName = String(item.interfaceName || '').trim(); - const area = String(item.area || '').trim() || 'backbone'; - if (!interfaceName) return; - const key = `${area.toUpperCase()}::${interfaceName.toUpperCase()}`; - if (!dedup.has(key)) { - dedup.set(key, { - rosId: item.rosId || null, - interfaceName, - area, - cost: item.cost, - }); - } - }); - }); - return { - serverId: group.representativeServerId, - templates: Array.from(dedup.values()), - }; - }), - }; - - const response = await api.post('/mikrotik/ospf-interface-templates/apply', payload); - const summary = response.data?.summary || {}; - const errors = Array.isArray(response.data?.errors) ? response.data.errors : []; - notify.success( - `OSPF сохранен: обновлено ${summary.updated || 0}, пропущено ${summary.skipped || 0}, не найдено ${summary.missing || 0}` - ); - if (Number(summary.invalid || 0) > 0) { - notify.warning(`Некорректных записей OSPF: ${summary.invalid}`); - } - if (errors.length > 0) { - notify.warning(`Ошибки применения OSPF: ${errors.length} роутер(ов)`); - } - } catch (error) { - console.error('Error applying OSPF templates:', error); - notify.error('Не удалось сохранить OSPF Interface Template в MikroTik'); - } finally { - setOspfSaving(false); - } - }; - - useEffect(() => { - if (activeTab !== 'interfaces') return; - if (!Array.isArray(servers) || servers.length === 0) return; - if (ospfAutoLoadedRef.current) return; - ospfAutoLoadedRef.current = true; - handleLoadOspfFromMikrotik({ silent: true }); - }, [activeTab, servers]); - // === Группировка по серверам === const gatewaysByServer = useMemo(() => { const grouped = {}; @@ -3933,111 +3666,13 @@ function NetworkConfigManager() { {activeTab === 'interfaces' && ( <>
-
-
-

OSPF Interface Template (HOME)

-
- Только для HOME роутеров. Источник данных — MikroTik, группировка по Area. Чем меньше cost, тем выше приоритет. -
+
+
+ Настройки OSPF вынесены в отдельный раздел проекта.
-
- - {config.ospfInterfaceTemplates?.length || 0} - - - -
-
-
- {ospfTemplatesByRouter.length === 0 ? ( -
- OSPF Interface Template не найдены на HOME роутерах. -
- ) : ( -
- {ospfTemplatesByRouter.map((group) => { - const areas = group.areas || []; - return ( -
-
-
-
{group.routerLabel}
-
-
- {areas.length === 0 ? ( -
- Нет доступных интерфейсов для OSPF шаблонов. -
- ) : ( - areas.map((areaGroup) => ( -
-
- Area: {areaGroup.area} -
-
- {areaGroup.items.map((template) => ( -
- setOspfDraggingItem({ - routerKey: group.routerKey, - area: areaGroup.area, - key: template.key, - }) - } - onDragOver={(e) => e.preventDefault()} - onDrop={() => - handleDropOspfTemplate(group.routerKey, areaGroup.area, template.key) - } - onDragEnd={() => setOspfDraggingItem(null)} - style={{ cursor: 'grab' }} - > -
- :: - {template.interfaceName} - - {template.active ? 'Активен' : 'Неактивен'} - -
- - cost {template.cost} - -
- ))} -
-
- )) - )} -
-
-
- ); - })} -
- )} + + Открыть Инструменты OSPF +
diff --git a/frontend/src/OspfToolsPage.jsx b/frontend/src/OspfToolsPage.jsx new file mode 100644 index 0000000..e53f517 --- /dev/null +++ b/frontend/src/OspfToolsPage.jsx @@ -0,0 +1,307 @@ +import { useEffect, useMemo, useState } from 'react'; +import { IconDatabase, IconRefresh, IconRoute2 } from '@tabler/icons-react'; +import PageHeader from './components/PageHeader.jsx'; +import api from './lib/api.js'; +import { useNotify } from './components/NotifyProvider.jsx'; + +const OSPF_COST_STEP = 10; + +function moveInArray(arr, fromIndex, toIndex) { + if (fromIndex < 0 || toIndex < 0 || fromIndex >= arr.length || toIndex >= arr.length) return arr; + const copy = [...arr]; + const [moved] = copy.splice(fromIndex, 1); + copy.splice(toIndex, 0, moved); + return copy; +} + +function isUnknownOspfInterfaceName(name) { + const value = String(name || '').trim(); + if (!value) return true; + if (value.startsWith('*')) return true; + if (value.toLowerCase() === 'unknown') return true; + return false; +} + +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 }; +} + +function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) { + const serverById = new Map((servers || []).map((s) => [String(s.id || s.ip || s.dns || ''), s])); + const byComposite = new Map(); + + (routerResults || []).forEach((result) => { + const serverId = String(result?.serverId || '').trim(); + if (!serverId) return; + const server = serverById.get(serverId) || { + id: serverId, + ip: String(result?.serverLabel || serverId), + dns: String(result?.serverLabel || ''), + }; + const identity = getOspfRouterIdentity(server); + const templates = Array.isArray(result?.templates) ? result.templates : []; + + templates.forEach((tpl) => { + const interfaceName = String(tpl?.interfaceName || '').trim(); + if (isUnknownOspfInterfaceName(interfaceName)) return; + const area = String(tpl?.area || '').trim() || 'backbone'; + const numericCost = Number(tpl?.cost); + const cost = Number.isFinite(numericCost) && numericCost >= 0 ? numericCost : OSPF_COST_STEP; + + const compositeKey = `${identity.key}::${area.toLowerCase()}::${interfaceName.toUpperCase()}`; + if (byComposite.has(compositeKey)) return; + + byComposite.set(compositeKey, { + key: compositeKey, + rosId: tpl?.id || null, + serverId, + routerKey: identity.key, + routerLabel: identity.label, + interfaceName, + area, + cost, + active: !Boolean(tpl?.disabled), + }); + }); + }); + + return Array.from(byComposite.values()); +} + +export default function OspfToolsPage() { + const notify = useNotify(); + const [servers, setServers] = useState([]); + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [dragging, setDragging] = useState(null); + + const groupedByRouter = useMemo(() => { + const grouped = {}; + (templates || []).forEach((item) => { + const sid = String(item?.serverId || ''); + const server = servers.find((s) => String(s.id || s.ip || s.dns || '') === sid); + const identity = getOspfRouterIdentity(server || { id: sid, ip: sid }); + if (!grouped[identity.key]) { + grouped[identity.key] = { + routerKey: identity.key, + routerLabel: identity.label, + representativeServerId: sid, + areas: {}, + }; + } + if (!grouped[identity.key].areas[item.area]) grouped[identity.key].areas[item.area] = []; + grouped[identity.key].areas[item.area].push(item); + }); + + return Object.values(grouped).map((group) => ({ + ...group, + areas: Object.entries(group.areas) + .map(([area, areaItems]) => ({ + area, + items: [...areaItems].sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0)), + })) + .sort((a, b) => a.area.localeCompare(b.area)), + })); + }, [templates, servers]); + + const loadServers = async () => { + try { + const response = await api.get('/servers'); + setServers(Array.isArray(response.data) ? response.data : []); + } catch (error) { + console.error('Failed to load servers:', error); + setServers([]); + } + }; + + const loadOspf = async ({ silent = false } = {}) => { + setLoading(true); + try { + const response = await api.get('/mikrotik/ospf-interface-templates'); + const results = Array.isArray(response.data?.results) ? response.data.results : []; + const errors = Array.isArray(response.data?.errors) ? response.data.errors : []; + setTemplates(buildOspfTemplatesFromRouterResults(results, servers)); + + if (!silent) { + if (results.length > 0) notify.success(`OSPF загружен: ${results.length} роутер(ов)`); + else notify.warning('OSPF Interface Template не найдены'); + if (errors.length > 0) notify.warning(`Ошибки чтения OSPF: ${errors.length}`); + } + } catch (error) { + console.error('Failed to load OSPF:', error); + if (!silent) notify.error('Не удалось загрузить OSPF с MikroTik'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadServers(); + }, []); + + useEffect(() => { + if (servers.length === 0) return; + loadOspf({ silent: true }); + }, [servers.length]); + + const onDrop = (routerKey, area, targetKey) => { + if (!dragging || dragging.routerKey !== routerKey || dragging.area !== area) return; + if (dragging.key === targetKey) { + setDragging(null); + return; + } + + setTemplates((prev) => { + const current = prev + .filter((item) => item.routerKey === routerKey && String(item.area) === String(area)) + .sort((a, b) => Number(a?.cost || 0) - Number(b?.cost || 0)); + const fromIndex = current.findIndex((item) => item.key === dragging.key); + const toIndex = current.findIndex((item) => item.key === targetKey); + if (fromIndex < 0 || toIndex < 0) return prev; + + const reordered = moveInArray(current, fromIndex, toIndex).map((item, idx) => ({ + ...item, + cost: (idx + 1) * OSPF_COST_STEP, + })); + + const other = prev.filter( + (item) => !(item.routerKey === routerKey && String(item.area) === String(area)) + ); + return [...other, ...reordered]; + }); + setDragging(null); + }; + + const saveOspf = async () => { + if (groupedByRouter.length === 0) { + notify.warning('Нет OSPF данных для сохранения'); + return; + } + setSaving(true); + try { + const payload = { + servers: groupedByRouter.map((group) => ({ + serverId: group.representativeServerId, + templates: group.areas.flatMap((areaGroup) => + areaGroup.items.map((item) => ({ + rosId: item.rosId || null, + interfaceName: item.interfaceName, + area: item.area, + cost: item.cost, + })) + ), + })), + }; + + const response = await api.post('/mikrotik/ospf-interface-templates/apply', payload); + const summary = response.data?.summary || {}; + notify.success(`OSPF сохранен: обновлено ${summary.updated || 0}, пропущено ${summary.skipped || 0}`); + } catch (error) { + console.error('Failed to save OSPF:', error); + notify.error('Не удалось сохранить OSPF в MikroTik'); + } finally { + setSaving(false); + } + }; + + return ( +
+ } + actions={( +
+ + +
+ )} + /> + +
+
+
+ Данные OSPF берутся напрямую с MikroTik. Группировка: роутер → Area → интерфейсы. + Чем меньше cost, тем выше приоритет. +
+
+
+ {groupedByRouter.length === 0 ? ( +
OSPF Interface Template не найдены.
+ ) : ( +
+ {groupedByRouter.map((routerGroup) => ( +
+
+
{routerGroup.routerLabel}
+
+ {routerGroup.areas.map((areaGroup) => ( +
+
+ Area: {areaGroup.area} +
+
+ {areaGroup.items.map((item) => ( +
setDragging({ + routerKey: routerGroup.routerKey, + area: areaGroup.area, + key: item.key, + })} + onDragOver={(e) => e.preventDefault()} + onDrop={() => onDrop(routerGroup.routerKey, areaGroup.area, item.key)} + onDragEnd={() => setDragging(null)} + style={{ cursor: 'grab' }} + > +
+ :: + {item.interfaceName} + + {item.active ? 'Активен' : 'Неактивен'} + +
+ cost {item.cost} +
+ ))} +
+
+ ))} +
+
+
+ ))} +
+ )} +
+
+
+ ); +} +