refactor(NetworkConfigManager): streamline IP registry processing; enhance conflict detection and improve interface display with clearer server information
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-01-22 17:33:31 +07:00
parent a076f981bb
commit 51999d485c
+47 -69
View File
@@ -477,72 +477,55 @@ function NetworkConfigManager() {
const ipRegistry = useMemo(() => { const ipRegistry = useMemo(() => {
const registry = []; const registry = [];
// Собираем IP из интерфейсов // Собираем IP из интерфейсов - каждая запись это отдельное использование IP
(config.tunnelInterfaces || []).forEach(iface => { (config.tunnelInterfaces || []).forEach(iface => {
const server = getServerInfo(iface.serverId);
const server2 = getServerInfo(iface.serverId2);
if (iface.localIp && iface.localIp.trim()) { if (iface.localIp && iface.localIp.trim()) {
const server = getServerInfo(iface.serverId);
registry.push({ registry.push({
ip: iface.localIp.trim(), ip: iface.localIp.trim(),
type: 'localIp', type: 'localIp',
interface: iface, interface: iface,
interfaceName: iface.name || iface.id, interfaceName: iface.name || iface.id,
server: server, server: server, // localIp привязан к serverId
server2: server2 serverName: server?.dns || server?.ip || iface.serverId || '—'
}); });
} }
if (iface.remoteIp && iface.remoteIp.trim()) { if (iface.remoteIp && iface.remoteIp.trim()) {
const server2 = getServerInfo(iface.serverId2);
registry.push({ registry.push({
ip: iface.remoteIp.trim(), ip: iface.remoteIp.trim(),
type: 'remoteIp', type: 'remoteIp',
interface: iface, interface: iface,
interfaceName: iface.name || iface.id, interfaceName: iface.name || iface.id,
server: server, server: server2, // remoteIp привязан к serverId2
server2: server2 serverName: server2?.dns || server2?.ip || iface.serverId2 || '—'
}); });
} }
}); });
// Группируем по IP адресам // Проверяем конфликты (один IP используется в нескольких интерфейсах)
const groupedByIp = {}; const ipUsageMap = {};
registry.forEach(item => { registry.forEach(item => {
if (!groupedByIp[item.ip]) { if (!ipUsageMap[item.ip]) {
groupedByIp[item.ip] = { ipUsageMap[item.ip] = [];
ip: item.ip,
interfaces: []
};
} }
groupedByIp[item.ip].interfaces.push({ ipUsageMap[item.ip].push(item);
type: item.type,
interface: item.interface,
interfaceName: item.interfaceName,
server: item.server,
server2: item.server2
});
}); });
// Преобразуем в массив и сортируем // Добавляем флаг конфликта
return Object.values(groupedByIp) return registry.map(item => ({
.map(item => ({ ...item,
...item, hasConflict: ipUsageMap[item.ip].length > 1
usageCount: item.interfaces.length, })).sort((a, b) => {
hasConflict: item.interfaces.length > 1, // Конфликт если IP используется в нескольких интерфейсах // Сначала конфликты, потом по IP, потом по типу
// Собираем уникальные серверы для этого IP if (a.hasConflict !== b.hasConflict) {
servers: [...new Set( return a.hasConflict ? -1 : 1;
item.interfaces.flatMap(i => [ }
i.server?.dns || i.server?.ip || i.interface.serverId, if (a.ip !== b.ip) {
i.server2?.dns || i.server2?.ip || i.interface.serverId2
].filter(Boolean))
)]
}))
.sort((a, b) => {
// Сначала конфликты, потом по IP
if (a.hasConflict !== b.hasConflict) {
return a.hasConflict ? -1 : 1;
}
return a.ip.localeCompare(b.ip); return a.ip.localeCompare(b.ip);
}); }
return a.type.localeCompare(b.type);
});
}, [config.tunnelInterfaces, servers]); }, [config.tunnelInterfaces, servers]);
// === Фильтрация реестра IP === // === Фильтрация реестра IP ===
@@ -551,7 +534,8 @@ function NetworkConfigManager() {
const term = ipRegistrySearch.toLowerCase(); const term = ipRegistrySearch.toLowerCase();
return ipRegistry.filter(item => return ipRegistry.filter(item =>
item.ip.toLowerCase().includes(term) || item.ip.toLowerCase().includes(term) ||
item.interfaces.some(i => i.interfaceName.toLowerCase().includes(term)) item.interfaceName.toLowerCase().includes(term) ||
item.serverName.toLowerCase().includes(term)
); );
}, [ipRegistry, ipRegistrySearch]); }, [ipRegistry, ipRegistrySearch]);
@@ -2005,14 +1989,15 @@ function NetworkConfigManager() {
<thead> <thead>
<tr> <tr>
<th>IP адрес</th> <th>IP адрес</th>
<th>Тип</th>
<th>Сервер</th> <th>Сервер</th>
<th>Интерфейсы</th> <th>Интерфейс</th>
<th className="text-end">Действия</th> <th className="text-end">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredIpRegistry.map((item, idx) => ( {filteredIpRegistry.map((item, idx) => (
<tr key={`${item.ip}-${idx}`} className={item.hasConflict ? 'table-danger' : ''}> <tr key={`${item.ip}-${item.type}-${item.interface.id}-${idx}`} className={item.hasConflict ? 'table-danger' : ''}>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<code className="fw-bold">{item.ip}</code> <code className="fw-bold">{item.ip}</code>
@@ -2024,33 +2009,26 @@ function NetworkConfigManager() {
</div> </div>
</td> </td>
<td> <td>
<div className="d-flex flex-column gap-1"> <span className={`badge ${item.type === 'localIp' ? 'bg-info-lt text-info' : 'bg-success-lt text-success'}`}>
{item.servers.map((serverName, sIdx) => ( {item.type === 'localIp' ? 'Local' : 'Remote'}
<div key={sIdx} className="d-flex align-items-center"> </span>
<IconServer size={14} className="text-muted me-1" /> </td>
<span>{serverName}</span> <td>
</div> <div className="d-flex align-items-center">
))} <IconServer size={14} className="text-muted me-1" />
{item.servers.length === 0 && <span className="text-muted"></span>} <span>{item.serverName}</span>
</div> </div>
</td> </td>
<td> <td>
<div className="d-flex flex-column gap-1"> <div className="d-flex align-items-center gap-2">
{item.interfaces.map((usage, uIdx) => ( <span>{item.interfaceName}</span>
<div key={uIdx} className="d-flex align-items-center gap-2"> <button
<span className={`badge ${usage.type === 'localIp' ? 'bg-info-lt text-info' : 'bg-success-lt text-success'}`}> className="btn btn-ghost-primary btn-icon btn-sm"
{usage.type === 'localIp' ? 'Local' : 'Remote'} onClick={() => handleEditInterface(item.interface)}
</span> title="Редактировать интерфейс"
<span>{usage.interfaceName}</span> >
<button <IconEdit size={14} />
className="btn btn-ghost-primary btn-icon btn-sm ms-auto" </button>
onClick={() => handleEditInterface(usage.interface)}
title="Редактировать интерфейс"
>
<IconEdit size={14} />
</button>
</div>
))}
</div> </div>
</td> </td>
<td className="text-end"> <td className="text-end">