feat(network-config-manager): enhance OSPF router identity handling and improve template management logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s

This commit is contained in:
2026-03-05 15:49:35 +07:00
parent e631ffab51
commit f77d71f22c
+96 -41
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo, useRef } from 'react';
import api from './lib/api.js'; import api from './lib/api.js';
import { useNotify } from './components/NotifyProvider.jsx'; import { useNotify } from './components/NotifyProvider.jsx';
import FormModal from './components/FormModal.jsx'; import FormModal from './components/FormModal.jsx';
@@ -291,6 +291,18 @@ function moveInArray(arr, fromIndex, toIndex) {
return copy; 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 = () => ({ const getEmptyGateway = () => ({
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
@@ -392,6 +404,7 @@ function NetworkConfigManager() {
const [ospfDraggingItem, setOspfDraggingItem] = useState(null); const [ospfDraggingItem, setOspfDraggingItem] = useState(null);
const [ospfLoading, setOspfLoading] = useState(false); const [ospfLoading, setOspfLoading] = useState(false);
const [ospfSaving, setOspfSaving] = useState(false); const [ospfSaving, setOspfSaving] = useState(false);
const ospfAutoLoadedRef = useRef(false);
// === IPSec Passwords Management === // === IPSec Passwords Management ===
const [ipsecPasswords, setIpsecPasswords] = useState([]); const [ipsecPasswords, setIpsecPasswords] = useState([]);
@@ -669,28 +682,45 @@ function NetworkConfigManager() {
}); });
}, [homeOspfCandidatesByServer]); }, [homeOspfCandidatesByServer]);
const ospfTemplatesByHomeServer = useMemo(() => { const ospfTemplatesByRouter = useMemo(() => {
const grouped = {}; const grouped = {};
(config.ospfInterfaceTemplates || []).forEach((item) => { (config.ospfInterfaceTemplates || []).forEach((item) => {
const sid = String(item?.serverId || ''); const sid = String(item?.serverId || '');
if (!sid) return; if (!sid) return;
if (!grouped[sid]) grouped[sid] = []; const server = getServerInfo(sid);
grouped[sid].push(item); 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(() => { if (serverFilter) {
let ids = Object.keys(homeOspfCandidatesByServer); const filterId = String(serverFilter);
if (serverFilter) ids = ids.filter((sid) => String(sid) === String(serverFilter)); return groups.filter((group) => group.serverIds.includes(filterId));
return ids; }
}, [homeOspfCandidatesByServer, serverFilter]);
const handleDropOspfTemplate = (serverId, targetKey) => { return groups;
if (!ospfDraggingItem || String(ospfDraggingItem.serverId) !== String(serverId)) return; }, [config.ospfInterfaceTemplates, serverFilter, servers]);
const handleDropOspfTemplate = (routerKey, targetKey) => {
if (!ospfDraggingItem || String(ospfDraggingItem.routerKey) !== String(routerKey)) return;
const draggedKey = String(ospfDraggingItem.key || ''); const draggedKey = String(ospfDraggingItem.key || '');
const destinationKey = String(targetKey || ''); const destinationKey = String(targetKey || '');
if (!draggedKey || !destinationKey || draggedKey === destinationKey) { if (!draggedKey || !destinationKey || draggedKey === destinationKey) {
@@ -700,7 +730,12 @@ function NetworkConfigManager() {
setConfig((prev) => { setConfig((prev) => {
const current = (prev.ospfInterfaceTemplates || []) 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)); .sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0));
const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey); const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey);
const toIndex = current.findIndex((item) => String(item.key || '') === destinationKey); const toIndex = current.findIndex((item) => String(item.key || '') === destinationKey);
@@ -711,9 +746,12 @@ function NetworkConfigManager() {
cost: (arr.length - idx) * OSPF_COST_STEP, cost: (arr.length - idx) * OSPF_COST_STEP,
})); }));
const otherServers = (prev.ospfInterfaceTemplates || []).filter( const otherServers = (prev.ospfInterfaceTemplates || []).filter((item) => {
(item) => String(item.serverId || '') !== String(serverId) const sid = String(item.serverId || '');
); const server = getServerInfo(sid);
const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
return identity.key !== String(routerKey);
});
return { return {
...prev, ...prev,
@@ -724,7 +762,7 @@ function NetworkConfigManager() {
setOspfDraggingItem(null); setOspfDraggingItem(null);
}; };
const handleLoadOspfFromMikrotik = async () => { const handleLoadOspfFromMikrotik = async ({ silent = false } = {}) => {
setOspfLoading(true); setOspfLoading(true);
try { try {
const response = await api.get('/mikrotik/ospf-interface-templates'); const response = await api.get('/mikrotik/ospf-interface-templates');
@@ -736,25 +774,26 @@ function NetworkConfigManager() {
ospfInterfaceTemplates: mergeOspfCostsFromRouter(prev.ospfInterfaceTemplates || [], results), ospfInterfaceTemplates: mergeOspfCostsFromRouter(prev.ospfInterfaceTemplates || [], results),
})); }));
if (results.length > 0) { if (!silent && results.length > 0) {
notify.success(`OSPF загружен с HOME роутеров: ${results.length}`); notify.success(`OSPF загружен с HOME роутеров: ${results.length}`);
} else { } else if (!silent) {
notify.warning('Не удалось получить OSPF шаблоны ни с одного HOME роутера'); notify.warning('Не удалось получить OSPF шаблоны ни с одного HOME роутера');
} }
if (errors.length > 0) { if (!silent && errors.length > 0) {
notify.warning(`Ошибки чтения OSPF: ${errors.length} роутер(ов)`); notify.warning(`Ошибки чтения OSPF: ${errors.length} роутер(ов)`);
} }
} catch (error) { } catch (error) {
console.error('Error loading OSPF templates:', error); console.error('Error loading OSPF templates:', error);
notify.error('Не удалось загрузить OSPF Interface Template с MikroTik'); if (!silent) {
notify.error('Не удалось загрузить OSPF Interface Template с MikroTik');
}
} finally { } finally {
setOspfLoading(false); setOspfLoading(false);
} }
}; };
const handleSaveOspfToMikrotik = async () => { const handleSaveOspfToMikrotik = async () => {
const serverEntries = Object.entries(ospfTemplatesByHomeServer); if (ospfTemplatesByRouter.length === 0) {
if (serverEntries.length === 0) {
notify.warning('Нет OSPF шаблонов для сохранения'); notify.warning('Нет OSPF шаблонов для сохранения');
return; return;
} }
@@ -762,13 +801,23 @@ function NetworkConfigManager() {
setOspfSaving(true); setOspfSaving(true);
try { try {
const payload = { const payload = {
servers: serverEntries.map(([serverId, templates]) => ({ servers: ospfTemplatesByRouter.map((group) => {
serverId, const dedup = new Map();
templates: templates.map((item) => ({ group.items.forEach((item) => {
interfaceName: item.interfaceName, const key = String(item.interfaceName || '').trim().toUpperCase();
cost: item.cost, 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); 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 gatewaysByServer = useMemo(() => {
const grouped = {}; const grouped = {};
@@ -4010,20 +4066,19 @@ function NetworkConfigManager() {
</div> </div>
</div> </div>
<div className="card-body"> <div className="card-body">
{ospfHomeServerIds.length === 0 ? ( {ospfTemplatesByRouter.length === 0 ? (
<div className="text-muted"> <div className="text-muted">
Для HOME роутеров пока нет GRE-интерфейсов. Создайте GRE-туннели, и шаблоны появятся автоматически. Для HOME роутеров пока нет GRE-интерфейсов. Создайте GRE-туннели, и шаблоны появятся автоматически.
</div> </div>
) : ( ) : (
<div className="row g-3"> <div className="row g-3">
{ospfHomeServerIds.map((serverId) => { {ospfTemplatesByRouter.map((group) => {
const server = getServerInfo(serverId); const templates = group.items || [];
const templates = ospfTemplatesByHomeServer[serverId] || [];
return ( return (
<div key={`ospf-home-${serverId}`} className="col-12 col-xl-6"> <div key={`ospf-router-${group.routerKey}`} className="col-12 col-xl-6">
<div className="card card-sm border"> <div className="card card-sm border">
<div className="card-header"> <div className="card-header">
<div className="fw-semibold">{server?.dns || server?.ip || serverId}</div> <div className="fw-semibold">{group.routerLabel}</div>
</div> </div>
<div className="list-group list-group-flush"> <div className="list-group list-group-flush">
{templates.length === 0 ? ( {templates.length === 0 ? (
@@ -4037,10 +4092,10 @@ function NetworkConfigManager() {
className="list-group-item d-flex align-items-center justify-content-between" className="list-group-item d-flex align-items-center justify-content-between"
draggable draggable
onDragStart={() => onDragStart={() =>
setOspfDraggingItem({ serverId, key: template.key }) setOspfDraggingItem({ routerKey: group.routerKey, key: template.key })
} }
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
onDrop={() => handleDropOspfTemplate(serverId, template.key)} onDrop={() => handleDropOspfTemplate(group.routerKey, template.key)}
onDragEnd={() => setOspfDraggingItem(null)} onDragEnd={() => setOspfDraggingItem(null)}
style={{ cursor: 'grab' }} style={{ cursor: 'grab' }}
> >