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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
This commit is contained in:
@@ -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) {
|
if (!parent) {
|
||||||
const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway';
|
code += `# Gateway "${gw.ip}" - родительский gateway/интерфейс "${parentRef.id}" не найден, пропущен\n`;
|
||||||
code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`;
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
validRoutesCount++;
|
// Определяем IP родителя в зависимости от типа
|
||||||
|
const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip;
|
||||||
|
|
||||||
// Создаём маршрут для рекурсивного gateway
|
if (!parentIp) {
|
||||||
const parentLabel = parent.parentType === 'interface'
|
const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway';
|
||||||
? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})`
|
code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`;
|
||||||
: `gateway ${parent.ip}`;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
code += `# Рекурсивный gateway: ${gw.ip}\n`;
|
validRoutesCount++;
|
||||||
code += `# Родительский: ${parentLabel}\n`;
|
|
||||||
|
|
||||||
// Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP
|
// Создаём маршрут для рекурсивного gateway
|
||||||
const gatewayValue = parent.parentType === 'interface'
|
const parentLabel = parent.parentType === 'interface'
|
||||||
? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}`
|
? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})`
|
||||||
: parentIp;
|
: `gateway ${parent.ip}`;
|
||||||
|
|
||||||
// Создаём маршрут к IP рекурсивного gateway через родительский
|
const distanceSuffix = parentRef.distance !== undefined && parentRef.distance !== null ? ` (distance: ${parentRef.distance})` : '';
|
||||||
const comment = gw.description
|
code += `# Рекурсивный gateway: ${gw.ip}${distanceSuffix}\n`;
|
||||||
? `${gw.description} -> ${parentIp}`
|
code += `# Родительский: ${parentLabel}\n`;
|
||||||
: `Recursive: ${gw.ip} -> ${parentIp}`;
|
|
||||||
code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue} comment="${comment}"\n`;
|
|
||||||
|
|
||||||
// Если у рекурсивного gateway есть описание с указанием на default route
|
// Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP
|
||||||
if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) {
|
const gatewayValue = parent.parentType === 'interface'
|
||||||
code += `# Дополнительный default route через рекурсивный gateway\n`;
|
? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}`
|
||||||
const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`;
|
: parentIp;
|
||||||
code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue} comment="${defaultComment}"\n`;
|
|
||||||
}
|
// Создаём маршрут к IP рекурсивного gateway через родительский
|
||||||
code += '\n';
|
const comment = gw.description
|
||||||
|
? `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>
|
||||||
|
|
||||||
{/* Зеленый блок с маршрутом */}
|
{/* Маршрут ip->ip сразу под названием */}
|
||||||
{displayText && (
|
{displayText && (
|
||||||
<span className="badge bg-success-lt text-success ms-auto" style={{ whiteSpace: 'nowrap', flexShrink: 0, fontSize: '0.875rem' }}>
|
<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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user