refactor(NetworkConfigManager): migrate parentGatewayId to parentGateways array for recursive gateways; enhance data handling and UI updates for improved clarity and functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s

This commit is contained in:
2026-01-22 15:23:18 +07:00
parent 8f81cf9645
commit 849247002c
+240 -110
View File
@@ -67,7 +67,7 @@ const getEmptyGateway = () => ({
serverId: '', serverId: '',
country: '', country: '',
type: 'direct', type: 'direct',
parentGatewayId: '', // Для рекурсивных - ID родительского gateway parentGateways: [], // Для рекурсивных - массив родительских gateway с distance [{id, distance}]
}); });
const getEmptyInterface = () => ({ const getEmptyInterface = () => ({
@@ -144,8 +144,21 @@ function NetworkConfigManager() {
try { try {
const response = await api.get('/network-config'); const response = await api.get('/network-config');
const data = response.data || getDefaultConfig(); const data = response.data || getDefaultConfig();
// Миграция старых данных: преобразуем parentGatewayId в parentGateways
const migratedGateways = (data.gateways || []).map(gw => {
if (gw.type === 'recursive' && gw.parentGatewayId && (!gw.parentGateways || gw.parentGateways.length === 0)) {
return {
...gw,
parentGateways: [{ id: gw.parentGatewayId, distance: undefined }],
parentGatewayId: undefined // Удаляем старое поле
};
}
return gw;
});
setConfig({ setConfig({
gateways: Array.isArray(data.gateways) ? data.gateways : [], gateways: Array.isArray(migratedGateways) ? migratedGateways : [],
tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [],
ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], ipPools: Array.isArray(data.ipPools) ? data.ipPools : [],
}); });
@@ -334,7 +347,17 @@ function NetworkConfigManager() {
}; };
const handleEditGateway = (gateway) => { const handleEditGateway = (gateway) => {
setEditingGateway({ ...gateway }); // Миграция старых данных при редактировании
const migratedGateway = gateway.type === 'recursive' && gateway.parentGatewayId && (!gateway.parentGateways || gateway.parentGateways.length === 0)
? {
...gateway,
parentGateways: [{ id: gateway.parentGatewayId, distance: undefined }]
}
: {
...gateway,
parentGateways: gateway.parentGateways || []
};
setEditingGateway(migratedGateway);
setGatewayModalMode('edit'); setGatewayModalMode('edit');
setGatewayModalOpen(true); setGatewayModalOpen(true);
}; };
@@ -346,36 +369,45 @@ function NetworkConfigManager() {
return; return;
} }
// Очищаем пустые parentGateways и удаляем старое поле parentGatewayId
const cleanedData = {
...gatewayData,
parentGateways: gatewayData.type === 'recursive'
? (gatewayData.parentGateways || []).filter(p => p && p.id)
: [],
parentGatewayId: undefined // Удаляем старое поле
};
if (gatewayModalMode === 'add') { if (gatewayModalMode === 'add') {
// Проверка на дубликат IP // Проверка на дубликат IP
const existingGateway = config.gateways.find(g => g.ip === gatewayData.ip.trim()); const existingGateway = config.gateways.find(g => g.ip === cleanedData.ip.trim());
if (existingGateway) { if (existingGateway) {
notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); notify.error(`Gateway с IP ${cleanedData.ip} уже существует`);
return; return;
} }
setConfig(prev => ({ setConfig(prev => ({
...prev, ...prev,
gateways: [...prev.gateways, gatewayData], gateways: [...prev.gateways, cleanedData],
})); }));
notify.success('Gateway добавлен'); notify.success('Gateway добавлен');
} else { } else {
// При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway // При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway
const currentGateway = config.gateways.find(g => g.id === gatewayData.id); const currentGateway = config.gateways.find(g => g.id === cleanedData.id);
if (!currentGateway) { if (!currentGateway) {
notify.error('Gateway не найден'); notify.error('Gateway не найден');
return; return;
} }
// Если IP изменился, проверяем, не занят ли он другим gateway // Если IP изменился, проверяем, не занят ли он другим gateway
if (gatewayData.ip.trim() !== currentGateway.ip && if (cleanedData.ip.trim() !== currentGateway.ip &&
config.gateways.some(g => g.id !== gatewayData.id && g.ip === gatewayData.ip.trim())) { config.gateways.some(g => g.id !== cleanedData.id && g.ip === cleanedData.ip.trim())) {
notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); notify.error(`Gateway с IP ${cleanedData.ip} уже существует`);
return; return;
} }
setConfig(prev => ({ setConfig(prev => ({
...prev, ...prev,
gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g), gateways: prev.gateways.map(g => g.id === cleanedData.id ? cleanedData : g),
})); }));
notify.success('Gateway обновлён'); notify.success('Gateway обновлён');
} }
@@ -586,7 +618,7 @@ function NetworkConfigManager() {
code += `# Связь с сервером: ${iface.server2Name}\n`; code += `# Связь с сервером: ${iface.server2Name}\n`;
code += `# Local IP: ${iface.localIp}\n`; code += `# Local IP: ${iface.localIp}\n`;
code += `# Remote IP: ${iface.remoteIp}\n`; code += `# Remote IP: ${iface.remoteIp}\n`;
code += `/ip address add address=${iface.localIp}/32 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`; code += `/ip address add address=${iface.localIp}/30 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`;
code += '\n'; code += '\n';
}); });
@@ -641,10 +673,13 @@ function NetworkConfigManager() {
let validRoutesCount = 0; let validRoutesCount = 0;
gateways.forEach(gw => { gateways.forEach(gw => {
const parent = getParentGateway(gw.parentGatewayId); // Поддержка старого формата (parentGatewayId) и нового (parentGateways)
const parentGatewaysList = gw.parentGateways && Array.isArray(gw.parentGateways) && gw.parentGateways.length > 0
? gw.parentGateways
: (gw.parentGatewayId ? [{ id: gw.parentGatewayId, distance: undefined }] : []);
if (!parent) { if (parentGatewaysList.length === 0) {
code += `# Gateway "${gw.ip || 'без IP'}" - родительский gateway/интерфейс не найден, пропущен\n`; code += `# Gateway "${gw.ip || 'без IP'}" - родительские gateway/интерфейсы не указаны, пропущен\n`;
return; return;
} }
@@ -653,43 +688,57 @@ function NetworkConfigManager() {
return; return;
} }
// Определяем IP родителя в зависимости от типа // Обрабатываем каждый родительский gateway
const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; parentGatewaysList.forEach((parentRef, parentIndex) => {
const parent = getParentGateway(parentRef.id);
if (!parentIp) {
const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway'; if (!parent) {
code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`; code += `# Gateway "${gw.ip}" - родительский gateway/интерфейс "${parentRef.id}" не найден, пропущен\n`;
return; return;
} }
validRoutesCount++; // Определяем IP родителя в зависимости от типа
const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip;
// Создаём маршрут для рекурсивного gateway
const parentLabel = parent.parentType === 'interface' if (!parentIp) {
? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})` const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway';
: `gateway ${parent.ip}`; code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`;
return;
code += `# Рекурсивный gateway: ${gw.ip}\n`; }
code += `# Родительский: ${parentLabel}\n`;
validRoutesCount++;
// Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP
const gatewayValue = parent.parentType === 'interface' // Создаём маршрут для рекурсивного gateway
? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` const parentLabel = parent.parentType === 'interface'
: parentIp; ? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})`
: `gateway ${parent.ip}`;
// Создаём маршрут к IP рекурсивного gateway через родительский
const comment = gw.description const distanceSuffix = parentRef.distance !== undefined && parentRef.distance !== null ? ` (distance: ${parentRef.distance})` : '';
? `${gw.description} -> ${parentIp}` code += `# Рекурсивный gateway: ${gw.ip}${distanceSuffix}\n`;
: `Recursive: ${gw.ip} -> ${parentIp}`; code += `# Родительский: ${parentLabel}\n`;
code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue} comment="${comment}"\n`;
// Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP
// Если у рекурсивного gateway есть описание с указанием на default route const gatewayValue = parent.parentType === 'interface'
if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}`
code += `# Дополнительный default route через рекурсивный gateway\n`; : parentIp;
const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`;
code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue} comment="${defaultComment}"\n`; // Создаём маршрут к IP рекурсивного gateway через родительский
} const comment = gw.description
code += '\n'; ? `Recursive: ${gw.description} -> ${parentIp}${distanceSuffix}`
: `Recursive: ${gw.ip} -> ${parentIp}${distanceSuffix}`;
// Формируем команду с distance, если указан
const distanceParam = parentRef.distance !== undefined && parentRef.distance !== null ? ` distance=${parentRef.distance}` : '';
code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue}${distanceParam} comment="${comment}"\n`;
// Если у рекурсивного gateway есть описание с указанием на default route
if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) {
code += `# Дополнительный default route через рекурсивный gateway${distanceSuffix}\n`;
const defaultComment = gw.description ? `Recursive: ${gw.description} (default)${distanceSuffix}` : `Recursive: ${gw.ip} (default)${distanceSuffix}`;
code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue}${distanceParam} comment="${defaultComment}"\n`;
}
code += '\n';
});
}); });
code += '# Проверка созданных маршрутов:\n'; code += '# Проверка созданных маршрутов:\n';
@@ -921,9 +970,16 @@ function NetworkConfigManager() {
const gatewayCountry = gateway.country || ''; const gatewayCountry = gateway.country || '';
const displayCountry = gatewayCountry || serverCountry; const displayCountry = gatewayCountry || serverCountry;
const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0];
const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId
? getParentGateway(gateway.parentGatewayId) // Поддержка старого формата (parentGatewayId) и нового (parentGateways)
: null; const parentGatewaysList = gateway.type === 'recursive'
? (gateway.parentGateways && Array.isArray(gateway.parentGateways) && gateway.parentGateways.length > 0
? gateway.parentGateways
: (gateway.parentGatewayId ? [{ id: gateway.parentGatewayId, distance: undefined }] : []))
: [];
// Получаем информацию о первом родительском gateway для отображения
const firstParent = parentGatewaysList.length > 0 ? getParentGateway(parentGatewaysList[0].id) : null;
// Определяем тип индикатора (как в ServerCard) // Определяем тип индикатора (как в ServerCard)
const indicatorType = gateway.type === 'recursive' ? 'EXIT' : 'JUMP'; const indicatorType = gateway.type === 'recursive' ? 'EXIT' : 'JUMP';
@@ -935,12 +991,16 @@ function NetworkConfigManager() {
if (gateway.type === 'direct') { if (gateway.type === 'direct') {
// Для прямого gateway: 0.0.0.0/0 -> IP адрес // Для прямого gateway: 0.0.0.0/0 -> IP адрес
displayText = gateway.ip ? `0.0.0.0/0 → ${gateway.ip}` : ''; displayText = gateway.ip ? `0.0.0.0/0 → ${gateway.ip}` : '';
} else if (gateway.type === 'recursive' && parentGateway) { } else if (gateway.type === 'recursive' && firstParent) {
// Для рекурсивного: IP адрес -> IP адрес родительского gateway // Для рекурсивного: IP адрес -> IP адрес первого родительского gateway
const parentIp = parentGateway.parentType === 'interface' const parentIp = firstParent.parentType === 'interface'
? parentGateway.remoteIp ? firstParent.remoteIp
: parentGateway.ip; : firstParent.ip;
displayText = gateway.ip && parentIp ? `${gateway.ip} → ${parentIp}` : ''; displayText = gateway.ip && parentIp ? `${gateway.ip} → ${parentIp}` : '';
// Если несколько родительских gateway, добавляем информацию
if (parentGatewaysList.length > 1) {
displayText += ` (+${parentGatewaysList.length - 1})`;
}
} }
return ( return (
@@ -967,36 +1027,32 @@ function NetworkConfigManager() {
</div> </div>
{/* Центральная часть: информация */} {/* Центральная часть: информация */}
<div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0, flex: '1 1 auto' }}> <div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0, flex: '1 1 auto', overflow: 'hidden' }}>
{/* Верхняя строка: флаг, название, провайдер */} {/* Верхняя строка: флаг, название */}
<div className="d-flex align-items-center mb-1 flex-wrap"> <div className="d-flex align-items-center mb-1">
<div className="d-flex align-items-center me-3" style={{ flex: '0 0 auto' }}> <div className="d-flex align-items-center" style={{ flex: '0 0 auto', minWidth: 0 }}>
{displayCountry && ( {displayCountry && (
<span className="me-2" style={{ fontSize: '1.25rem', flexShrink: 0 }}>{countryToFlag(displayCountry)}</span> <span className="me-2" style={{ fontSize: '1.25rem', flexShrink: 0 }}>{countryToFlag(displayCountry)}</span>
)} )}
<div style={{ whiteSpace: 'nowrap' }}> <div>
<span className="fw-semibold">{gateway.description || provider || gateway.ip || '—'}</span> <span className="fw-semibold">{gateway.description || gateway.ip || '—'}</span>
{provider && (
<>
<span className="text-muted mx-2">•</span>
<span className="text-muted">{provider}</span>
</>
)}
</div> </div>
</div> </div>
</div>
{/* Зеленый блок с маршрутом */}
{displayText && ( {/* Маршрут ip->ip сразу под названием */}
<span className="badge bg-success-lt text-success ms-auto" style={{ whiteSpace: 'nowrap', flexShrink: 0, fontSize: '0.875rem' }}> {displayText && (
<div className="mb-1">
<span className="badge bg-success-lt text-success" style={{ whiteSpace: 'nowrap', fontSize: '0.875rem' }}>
{displayText} {displayText}
</span> </span>
)} </div>
</div> )}
{/* Нижняя строка: DNS + badges */} {/* Нижняя строка: DNS + badges */}
<div className="d-flex align-items-center gap-2 flex-wrap"> <div className="d-flex align-items-center gap-2 flex-wrap">
{server && ( {server && (
<code className="text-muted" style={{ fontSize: '0.8rem', wordBreak: 'break-all' }}> <code className="text-muted" style={{ fontSize: '0.8rem', wordBreak: 'break-all', overflowWrap: 'break-word' }}>
{server.dns || server.ip || gateway.serverId} {server.dns || server.ip || gateway.serverId}
</code> </code>
)} )}
@@ -1102,30 +1158,32 @@ function NetworkConfigManager() {
</div> </div>
{/* Центральная часть: информация */} {/* Центральная часть: информация */}
<div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0, flex: '1 1 auto' }}> <div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0, flex: '1 1 auto', overflow: 'hidden' }}>
{/* Верхняя строка: флаг, название */} {/* Верхняя строка: флаг, название */}
<div className="d-flex align-items-center mb-1 flex-wrap"> <div className="d-flex align-items-center mb-1">
<div className="d-flex align-items-center me-3" style={{ flex: '0 0 auto' }}> <div className="d-flex align-items-center" style={{ flex: '0 0 auto', minWidth: 0 }}>
{displayCountry && ( {displayCountry && (
<span className="me-2" style={{ fontSize: '1.25rem', flexShrink: 0 }}>{countryToFlag(displayCountry)}</span> <span className="me-2" style={{ fontSize: '1.25rem', flexShrink: 0 }}>{countryToFlag(displayCountry)}</span>
)} )}
<div style={{ whiteSpace: 'nowrap' }}> <div>
<span className="fw-semibold">{interfaceName}</span> <span className="fw-semibold">{interfaceName}</span>
</div> </div>
</div> </div>
{/* Зеленый блок с IP */}
{displayIp && displayIp !== '—' && (
<span className="badge bg-success-lt text-success ms-auto" style={{ whiteSpace: 'nowrap', flexShrink: 0, fontSize: '0.875rem' }}>
→ {displayIp}
</span>
)}
</div> </div>
{/* Маршрут между Local IP и Remote IP сразу под названием */}
{displayLocalIp !== '—' && displayRemoteIp !== '—' && (
<div className="mb-1">
<span className="badge bg-success-lt text-success" style={{ whiteSpace: 'nowrap', fontSize: '0.875rem' }}>
{displayLocalIp} → {displayRemoteIp}
</span>
</div>
)}
{/* Нижняя строка: DNS + badges */} {/* Нижняя строка: DNS + badges */}
<div className="d-flex align-items-center gap-2 flex-wrap"> <div className="d-flex align-items-center gap-2 flex-wrap">
{displayServerName && ( {displayServerName && (
<code className="text-muted" style={{ fontSize: '0.8rem', wordBreak: 'break-all' }}> <code className="text-muted" style={{ fontSize: '0.8rem', wordBreak: 'break-all', overflowWrap: 'break-word' }}>
{displayServerName} {displayServerName}
</code> </code>
)} )}
@@ -1447,9 +1505,13 @@ function NetworkConfigManager() {
const gatewayCountry = gateway.country || ''; const gatewayCountry = gateway.country || '';
const displayCountry = gatewayCountry || serverCountry; const displayCountry = gatewayCountry || serverCountry;
const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0];
const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId // Поддержка старого формата (parentGatewayId) и нового (parentGateways)
? getParentGateway(gateway.parentGatewayId) const parentGatewaysList = gateway.type === 'recursive'
: null; ? (gateway.parentGateways && Array.isArray(gateway.parentGateways) && gateway.parentGateways.length > 0
? gateway.parentGateways
: (gateway.parentGatewayId ? [{ id: gateway.parentGatewayId, distance: undefined }] : []))
: [];
const firstParent = parentGatewaysList.length > 0 ? getParentGateway(parentGatewaysList[0].id) : null;
return ( return (
<tr key={gateway.id}> <tr key={gateway.id}>
@@ -1482,14 +1544,17 @@ function NetworkConfigManager() {
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`}> <span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`}>
{gatewayType.label} {gatewayType.label}
</span> </span>
{parentGateway && ( {firstParent && (
<div className="text-muted small mt-1"> <div className="text-muted small mt-1">
→ {parentGateway.parentType === 'interface' → {firstParent.parentType === 'interface'
? `Интерфейс ${parentGateway.name || parentGateway.interfaceType || 'interface'} (${parentGateway.remoteIp || '—'})` ? `Интерфейс ${firstParent.name || firstParent.interfaceType || 'interface'} (${firstParent.remoteIp || '—'})`
: parentGateway.ip : firstParent.ip
? `Gateway ${parentGateway.ip}` ? `Gateway ${firstParent.ip}`
: 'Gateway (не указан IP)' : 'Gateway (не указан IP)'
} }
{parentGatewaysList.length > 1 && (
<span className="ms-1">(+{parentGatewaysList.length - 1})</span>
)}
</div> </div>
)} )}
</td> </td>
@@ -1826,7 +1891,7 @@ function NetworkConfigManager() {
name="type" name="type"
type="select" type="select"
value={editingGateway.type} value={editingGateway.type}
onChange={(val) => setEditingGateway({ ...editingGateway, type: val, parentGatewayId: val === 'direct' ? '' : editingGateway.parentGatewayId })} onChange={(val) => setEditingGateway({ ...editingGateway, type: val, parentGateways: val === 'direct' ? [] : (editingGateway.parentGateways || []) })}
options={GATEWAY_TYPES.map(t => ({ value: t.value, label: `${t.label} - ${t.description}` }))} options={GATEWAY_TYPES.map(t => ({ value: t.value, label: `${t.label} - ${t.description}` }))}
required required
/> />
@@ -1843,17 +1908,82 @@ function NetworkConfigManager() {
</div> </div>
{editingGateway.type === 'recursive' && ( {editingGateway.type === 'recursive' && (
<div className="col-12"> <div className="col-12">
<label className="form-label">Родительский gateway или интерфейс</label> <label className="form-label">Родительские gateway или интерфейсы</label>
<GatewayAutocompleteInput <div className="d-flex flex-column gap-2">
value={editingGateway.parentGatewayId} {(editingGateway.parentGateways || []).map((parent, index) => {
onChange={(val) => setEditingGateway({ ...editingGateway, parentGatewayId: val })} const parentInfo = getParentGateway(parent.id);
gateways={config.gateways} return (
interfaces={config.tunnelInterfaces} <div key={index} className="card card-sm border">
excludeGatewayId={editingGateway.id} <div className="card-body p-2">
placeholder="Выберите родительский gateway или интерфейс..." <div className="row g-2 align-items-center">
/> <div className="col-md-8">
<GatewayAutocompleteInput
value={parent.id}
onChange={(val) => {
const newParents = [...(editingGateway.parentGateways || [])];
newParents[index] = { ...newParents[index], id: val };
setEditingGateway({ ...editingGateway, parentGateways: newParents });
}}
gateways={config.gateways}
interfaces={config.tunnelInterfaces}
excludeGatewayId={editingGateway.id}
placeholder="Выберите родительский gateway или интерфейс..."
/>
</div>
<div className="col-md-3">
<FormField
label="Distance"
name="distance"
type="number"
value={parent.distance || ''}
onChange={(val) => {
const newParents = [...(editingGateway.parentGateways || [])];
newParents[index] = { ...newParents[index], distance: val ? parseInt(val) : undefined };
setEditingGateway({ ...editingGateway, parentGateways: newParents });
}}
placeholder="1"
/>
</div>
<div className="col-md-1 d-flex align-items-end">
<button
type="button"
className="btn btn-outline-danger btn-sm"
onClick={() => {
const newParents = (editingGateway.parentGateways || []).filter((_, i) => i !== index);
setEditingGateway({ ...editingGateway, parentGateways: newParents });
}}
title="Удалить"
>
<IconTrash size={16} />
</button>
</div>
</div>
{parentInfo && (
<div className="mt-2 small text-muted">
{parentInfo.parentType === 'interface'
? `Интерфейс ${parentInfo.name || parentInfo.interfaceType || 'interface'} (${parentInfo.remoteIp || '—'})`
: `Gateway ${parentInfo.ip || '—'}`
}
</div>
)}
</div>
</div>
);
})}
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={() => {
const newParents = [...(editingGateway.parentGateways || []), { id: '', distance: undefined }];
setEditingGateway({ ...editingGateway, parentGateways: newParents });
}}
>
<IconPlus size={16} className="me-1" />
Добавить родительский gateway
</button>
</div>
<div className="form-text"> <div className="form-text">
Рекурсивный gateway может ссылаться на прямой gateway или на интерфейс (по его remote IP) Рекурсивный gateway может ссылаться на несколько прямых gateway или интерфейсов. Distance определяет приоритет маршрута (меньше = выше приоритет).
</div> </div>
</div> </div>
)} )}