feat(NetworkConfigManager): enhance gateway IP generation logic to support direct and recursive gateways, improving template-based configuration and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
This commit is contained in:
@@ -547,9 +547,17 @@ function NetworkConfigManager() {
|
|||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
// Инициализируем gateways из шаблона
|
// Инициализируем gateways из шаблона
|
||||||
const initializedGateways = template.gateways.map((gw, index) => {
|
const initializedGateways = template.gateways.map((gw, index) => {
|
||||||
// Генерируем IP из шаблона, если есть базовый IP
|
let ip = '';
|
||||||
let ip = gw.ipTemplate || '';
|
|
||||||
if (templateBaseIp && gw.ipTemplate) {
|
// Для прямых gateways используем IP сервера (если выбран сервер)
|
||||||
|
if (gw.type === 'direct' && templateServerId) {
|
||||||
|
const server = getServerInfo(templateServerId);
|
||||||
|
if (server && server.ip) {
|
||||||
|
ip = server.ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Для рекурсивных gateways генерируем из базового IP
|
||||||
|
else if (gw.type === 'recursive' && templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && gw.ipTemplate) {
|
||||||
ip = gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
ip = gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,12 +573,21 @@ function NetworkConfigManager() {
|
|||||||
setTemplateGateways(initializedGateways);
|
setTemplateGateways(initializedGateways);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обновление IP адресов при изменении базового IP
|
// Обновление IP адресов при изменении базового IP или сервера
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTemplate && templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && templateGateways.length > 0) {
|
if (selectedTemplate && templateGateways.length > 0) {
|
||||||
const updatedGateways = templateGateways.map((gw, index) => {
|
const updatedGateways = templateGateways.map((gw, index) => {
|
||||||
const originalTemplate = selectedTemplate.gateways[index];
|
const originalTemplate = selectedTemplate.gateways[index];
|
||||||
if (originalTemplate && originalTemplate.ipTemplate) {
|
|
||||||
|
// Для прямых gateways используем IP сервера
|
||||||
|
if (gw.type === 'direct' && templateServerId) {
|
||||||
|
const server = getServerInfo(templateServerId);
|
||||||
|
if (server && server.ip) {
|
||||||
|
return { ...gw, ip: server.ip };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Для рекурсивных gateways генерируем из базового IP
|
||||||
|
else if (gw.type === 'recursive' && templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && originalTemplate && originalTemplate.ipTemplate) {
|
||||||
const templateIp = originalTemplate.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
const templateIp = originalTemplate.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
// Обновляем IP только если он соответствует шаблону (последний октет совпадает) или пустой
|
// Обновляем IP только если он соответствует шаблону (последний октет совпадает) или пустой
|
||||||
const currentIpParts = gw.ip ? gw.ip.split('.') : [];
|
const currentIpParts = gw.ip ? gw.ip.split('.') : [];
|
||||||
@@ -591,7 +608,7 @@ function NetworkConfigManager() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [templateBaseIp, selectedTemplate?.id]);
|
}, [templateBaseIp, templateServerId, selectedTemplate?.id]);
|
||||||
|
|
||||||
const handleCreateGatewaysFromTemplate = () => {
|
const handleCreateGatewaysFromTemplate = () => {
|
||||||
if (!selectedTemplate) {
|
if (!selectedTemplate) {
|
||||||
@@ -619,12 +636,21 @@ function NetworkConfigManager() {
|
|||||||
const newGateways = [];
|
const newGateways = [];
|
||||||
const usedIps = new Set(config.gateways.map(g => g.ip));
|
const usedIps = new Set(config.gateways.map(g => g.ip));
|
||||||
|
|
||||||
|
// Создаем маппинг между ID из шаблона и реальными ID после создания
|
||||||
|
const templateIdToRealId = new Map();
|
||||||
|
|
||||||
templateGateways.forEach((gwTemplate, index) => {
|
templateGateways.forEach((gwTemplate, index) => {
|
||||||
// Используем IP из редактируемого gateway (уже сгенерированный из шаблона)
|
|
||||||
let ip = gwTemplate.ip || '';
|
let ip = gwTemplate.ip || '';
|
||||||
|
|
||||||
// Если IP пустой, пытаемся сгенерировать из шаблона
|
// Для прямых gateways используем IP сервера
|
||||||
if (!ip && selectedTemplate.gateways[index]?.ipTemplate) {
|
if (gwTemplate.type === 'direct' && templateServerId) {
|
||||||
|
const server = getServerInfo(templateServerId);
|
||||||
|
if (server && server.ip) {
|
||||||
|
ip = server.ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Для рекурсивных gateways генерируем из базового IP
|
||||||
|
else if (gwTemplate.type === 'recursive' && !ip && selectedTemplate.gateways[index]?.ipTemplate) {
|
||||||
ip = selectedTemplate.gateways[index].ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
ip = selectedTemplate.gateways[index].ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,15 +668,24 @@ function NetworkConfigManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newGatewayId = `gw-${Date.now()}-${index}-${Math.random().toString(16).slice(2, 6)}`;
|
||||||
|
|
||||||
|
// Сохраняем маппинг ID
|
||||||
|
templateIdToRealId.set(gwTemplate.id, newGatewayId);
|
||||||
|
|
||||||
const newGateway = {
|
const newGateway = {
|
||||||
id: `gw-${Date.now()}-${index}-${Math.random().toString(16).slice(2, 6)}`,
|
id: newGatewayId,
|
||||||
ip: ip,
|
ip: ip,
|
||||||
description: gwTemplate.description || '',
|
description: gwTemplate.description || '',
|
||||||
serverId: templateServerId,
|
serverId: templateServerId,
|
||||||
country: gwTemplate.country || '',
|
country: gwTemplate.country || '',
|
||||||
type: gwTemplate.type,
|
type: gwTemplate.type,
|
||||||
parentGateways: gwTemplate.type === 'recursive'
|
parentGateways: gwTemplate.type === 'recursive'
|
||||||
? (gwTemplate.parentGateways || []).filter(p => p && p.id)
|
? (gwTemplate.parentGateways || []).filter(p => p && p.id).map(p => {
|
||||||
|
// Преобразуем ID родительского gateway из шаблона в реальный ID
|
||||||
|
const realParentId = templateIdToRealId.get(p.id) || p.id;
|
||||||
|
return { ...p, id: realParentId };
|
||||||
|
})
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2569,9 +2604,21 @@ function NetworkConfigManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// === Получение родительского gateway или интерфейса ===
|
// === Получение родительского gateway или интерфейса ===
|
||||||
const getParentGateway = (parentId) => {
|
const getParentGateway = (parentId, templateGatewaysList = null) => {
|
||||||
if (!parentId) return null;
|
if (!parentId) return null;
|
||||||
|
|
||||||
|
// Если передан список templateGateways, сначала ищем там
|
||||||
|
if (templateGatewaysList) {
|
||||||
|
const templateGateway = templateGatewaysList.find(g => g.id === parentId);
|
||||||
|
if (templateGateway) {
|
||||||
|
return {
|
||||||
|
parentType: 'gateway',
|
||||||
|
...templateGateway,
|
||||||
|
ip: templateGateway.ip
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Сначала ищем в gateway
|
// Сначала ищем в gateway
|
||||||
const gateway = config.gateways?.find(g => g.id === parentId);
|
const gateway = config.gateways?.find(g => g.id === parentId);
|
||||||
if (gateway) {
|
if (gateway) {
|
||||||
@@ -2598,7 +2645,8 @@ function NetworkConfigManager() {
|
|||||||
// Если не найден ни gateway, ни интерфейс - логируем для отладки
|
// Если не найден ни gateway, ни интерфейс - логируем для отладки
|
||||||
console.warn('Parent gateway/interface not found:', parentId, {
|
console.warn('Parent gateway/interface not found:', parentId, {
|
||||||
availableGateways: config.gateways?.map(g => g.id),
|
availableGateways: config.gateways?.map(g => g.id),
|
||||||
availableInterfaces: config.tunnelInterfaces?.map(i => i.id)
|
availableInterfaces: config.tunnelInterfaces?.map(i => i.id),
|
||||||
|
templateGateways: templateGatewaysList?.map(g => g.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -5051,15 +5099,21 @@ function NetworkConfigManager() {
|
|||||||
<label className="form-label required">Шаблон набора gateways</label>
|
<label className="form-label required">Шаблон набора gateways</label>
|
||||||
<div className="row g-2">
|
<div className="row g-2">
|
||||||
{GATEWAY_TEMPLATES.map(template => {
|
{GATEWAY_TEMPLATES.map(template => {
|
||||||
// Генерируем предпросмотр IP адресов если есть базовый IP
|
// Генерируем предпросмотр IP адресов если есть базовый IP и сервер
|
||||||
const previewIps = templateBaseIp && templateBaseIp.trim().split('.').length >= 3
|
const previewIps = template.gateways.map((gw, idx) => {
|
||||||
? template.gateways.map(gw => {
|
// Для прямых gateways показываем IP сервера, если выбран
|
||||||
if (gw.ipTemplate) {
|
if (gw.type === 'direct' && templateServerId) {
|
||||||
return gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
const server = getServerInfo(templateServerId);
|
||||||
}
|
if (server && server.ip) {
|
||||||
return '—';
|
return server.ip;
|
||||||
})
|
}
|
||||||
: template.gateways.map(() => '—');
|
}
|
||||||
|
// Для рекурсивных gateways генерируем из базового IP
|
||||||
|
if (gw.type === 'recursive' && templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && gw.ipTemplate) {
|
||||||
|
return gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={template.id} className="col-12 col-md-6">
|
<div key={template.id} className="col-12 col-md-6">
|
||||||
@@ -5085,7 +5139,7 @@ function NetworkConfigManager() {
|
|||||||
{template.gateways.filter(g => g.type === 'direct').length} прямых, {template.gateways.filter(g => g.type === 'recursive').length} рекурсивных
|
{template.gateways.filter(g => g.type === 'direct').length} прямых, {template.gateways.filter(g => g.type === 'recursive').length} рекурсивных
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && (
|
{templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && templateServerId && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="text-muted small mb-1">Предпросмотр IP:</div>
|
<div className="text-muted small mb-1">Предпросмотр IP:</div>
|
||||||
<div className="d-flex flex-wrap gap-1">
|
<div className="d-flex flex-wrap gap-1">
|
||||||
@@ -5138,7 +5192,7 @@ function NetworkConfigManager() {
|
|||||||
placeholder="10.9.9 или 45.8.248 (минимум 3 октета)"
|
placeholder="10.9.9 или 45.8.248 (минимум 3 октета)"
|
||||||
/>
|
/>
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
Базовый IP для генерации адресов gateways. Например, для "10.9.9" будут созданы IP: 10.9.9.1, 10.9.9.2, 10.9.9.3 и т.д.
|
Базовый IP для генерации адресов рекурсивных gateways. Например, для "10.9.9" будут созданы IP: 10.9.9.1, 10.9.9.2, 10.9.9.3 и т.д. Прямые gateways будут использовать IP адрес выбранного сервера.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -5188,10 +5242,17 @@ function NetworkConfigManager() {
|
|||||||
newGateways[index].ip = e.target.value;
|
newGateways[index].ip = e.target.value;
|
||||||
setTemplateGateways(newGateways);
|
setTemplateGateways(newGateways);
|
||||||
}}
|
}}
|
||||||
placeholder={gw.ipTemplate ? gw.ipTemplate.replace('{baseIp}', templateBaseIp || 'X.X.X') : 'IP адрес'}
|
placeholder={
|
||||||
|
gw.type === 'direct' && templateServerId
|
||||||
|
? (getServerInfo(templateServerId)?.ip || 'IP сервера')
|
||||||
|
: (gw.ipTemplate ? gw.ipTemplate.replace('{baseIp}', templateBaseIp || 'X.X.X') : 'IP адрес')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="form-text small">
|
<div className="form-text small">
|
||||||
Шаблон: {gw.ipTemplate || '—'}
|
{gw.type === 'direct'
|
||||||
|
? (templateServerId ? `IP сервера: ${getServerInfo(templateServerId)?.ip || '—'}` : 'Выберите сервер для автоматической подстановки IP')
|
||||||
|
: `Шаблон: ${gw.ipTemplate || '—'}`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
@@ -5214,7 +5275,7 @@ function NetworkConfigManager() {
|
|||||||
<label className="form-label small">Родительские gateway/интерфейсы</label>
|
<label className="form-label small">Родительские gateway/интерфейсы</label>
|
||||||
<div className="d-flex flex-column gap-2">
|
<div className="d-flex flex-column gap-2">
|
||||||
{(gw.parentGateways || []).map((parent, parentIndex) => {
|
{(gw.parentGateways || []).map((parent, parentIndex) => {
|
||||||
const parentInfo = getParentGateway(parent.id);
|
const parentInfo = getParentGateway(parent.id, templateGateways);
|
||||||
return (
|
return (
|
||||||
<div key={parentIndex} className="d-flex align-items-center gap-2">
|
<div key={parentIndex} className="d-flex align-items-center gap-2">
|
||||||
<div className="flex-grow-1">
|
<div className="flex-grow-1">
|
||||||
@@ -5230,6 +5291,8 @@ function NetworkConfigManager() {
|
|||||||
}}
|
}}
|
||||||
gateways={config.gateways}
|
gateways={config.gateways}
|
||||||
interfaces={config.tunnelInterfaces}
|
interfaces={config.tunnelInterfaces}
|
||||||
|
templateGateways={templateGateways}
|
||||||
|
excludeGatewayId={gw.id}
|
||||||
placeholder="Выберите родительский gateway или интерфейс..."
|
placeholder="Выберите родительский gateway или интерфейс..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ function GatewayAutocompleteInput({
|
|||||||
onChange,
|
onChange,
|
||||||
gateways = [],
|
gateways = [],
|
||||||
interfaces = [],
|
interfaces = [],
|
||||||
|
templateGateways = [], // Gateways из шаблона, которые еще не созданы
|
||||||
excludeGatewayId = null, // ID gateway, который нужно исключить (текущий редактируемый)
|
excludeGatewayId = null, // ID gateway, который нужно исключить (текущий редактируемый)
|
||||||
placeholder = '',
|
placeholder = '',
|
||||||
className = 'form-control',
|
className = 'form-control',
|
||||||
@@ -25,7 +26,23 @@ function GatewayAutocompleteInput({
|
|||||||
const allItems = useMemo(() => {
|
const allItems = useMemo(() => {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
// Добавляем прямые gateway
|
// Добавляем прямые gateway из шаблона (которые еще не созданы)
|
||||||
|
templateGateways
|
||||||
|
.filter(g => g.id !== excludeGatewayId && g.type === 'direct' && g.ip)
|
||||||
|
.forEach(g => {
|
||||||
|
items.push({
|
||||||
|
type: 'gateway',
|
||||||
|
id: g.id,
|
||||||
|
value: g.id,
|
||||||
|
label: g.ip,
|
||||||
|
description: g.description || '',
|
||||||
|
ip: g.ip,
|
||||||
|
isTemplate: true, // Помечаем как шаблонный gateway
|
||||||
|
search: [g.ip, g.description, 'gateway', 'template'].filter(Boolean).join(' ').toLowerCase(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем прямые gateway (уже созданные)
|
||||||
gateways
|
gateways
|
||||||
.filter(g => g.id !== excludeGatewayId && g.type === 'direct' && g.ip)
|
.filter(g => g.id !== excludeGatewayId && g.type === 'direct' && g.ip)
|
||||||
.forEach(g => {
|
.forEach(g => {
|
||||||
@@ -36,6 +53,7 @@ function GatewayAutocompleteInput({
|
|||||||
label: g.ip,
|
label: g.ip,
|
||||||
description: g.description || '',
|
description: g.description || '',
|
||||||
ip: g.ip,
|
ip: g.ip,
|
||||||
|
isTemplate: false,
|
||||||
search: [g.ip, g.description, 'gateway'].filter(Boolean).join(' ').toLowerCase(),
|
search: [g.ip, g.description, 'gateway'].filter(Boolean).join(' ').toLowerCase(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -57,7 +75,7 @@ function GatewayAutocompleteInput({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [gateways, interfaces, excludeGatewayId]);
|
}, [gateways, interfaces, templateGateways, excludeGatewayId]);
|
||||||
|
|
||||||
const suggestions = useMemo(() => {
|
const suggestions = useMemo(() => {
|
||||||
const q = String(value || '').toLowerCase();
|
const q = String(value || '').toLowerCase();
|
||||||
@@ -182,16 +200,17 @@ function GatewayAutocompleteInput({
|
|||||||
onMouseEnter={() => setActiveIndex(globalIdx)}
|
onMouseEnter={() => setActiveIndex(globalIdx)}
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-start">
|
<div className="d-flex align-items-start">
|
||||||
<span className="avatar me-2 bg-green-lt text-green border-0" style={{ width: 24, height: 24 }}>
|
<span className={`avatar me-2 ${s.isTemplate ? 'bg-yellow-lt text-yellow' : 'bg-green-lt text-green'} border-0`} style={{ width: 24, height: 24 }}>
|
||||||
<IconWorld size={14} />
|
<IconWorld size={14} />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-fill text-start">
|
<div className="flex-fill text-start">
|
||||||
<div className="fw-medium">
|
<div className="fw-medium">
|
||||||
<code>{s.label}</code>
|
<code>{s.label}</code>
|
||||||
{s.description && <span className="text-muted ms-2">({s.description})</span>}
|
{s.description && <span className="text-muted ms-2">({s.description})</span>}
|
||||||
|
{s.isTemplate && <span className="badge bg-yellow-lt text-yellow ms-2" style={{ fontSize: '0.7rem' }}>Шаблон</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted small text-truncate" style={{ maxWidth: '100%' }}>
|
<div className="text-muted small text-truncate" style={{ maxWidth: '100%' }}>
|
||||||
Gateway
|
Gateway {s.isTemplate ? '(из шаблона)' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user