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 { 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() {
</div>
</div>
<div className="card-body">
{ospfHomeServerIds.length === 0 ? (
{ospfTemplatesByRouter.length === 0 ? (
<div className="text-muted">
Для HOME роутеров пока нет GRE-интерфейсов. Создайте GRE-туннели, и шаблоны появятся автоматически.
</div>
) : (
<div className="row g-3">
{ospfHomeServerIds.map((serverId) => {
const server = getServerInfo(serverId);
const templates = ospfTemplatesByHomeServer[serverId] || [];
{ospfTemplatesByRouter.map((group) => {
const templates = group.items || [];
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-header">
<div className="fw-semibold">{server?.dns || server?.ip || serverId}</div>
<div className="fw-semibold">{group.routerLabel}</div>
</div>
<div className="list-group list-group-flush">
{templates.length === 0 ? (
@@ -4037,10 +4092,10 @@ function NetworkConfigManager() {
className="list-group-item d-flex align-items-center justify-content-between"
draggable
onDragStart={() =>
setOspfDraggingItem({ serverId, key: template.key })
setOspfDraggingItem({ routerKey: group.routerKey, key: template.key })
}
onDragOver={(e) => e.preventDefault()}
onDrop={() => handleDropOspfTemplate(serverId, template.key)}
onDrop={() => handleDropOspfTemplate(group.routerKey, template.key)}
onDragEnd={() => setOspfDraggingItem(null)}
style={{ cursor: 'grab' }}
>