feat(NetworkConfigManager): implement dynamic server pair management for multiple tunnel creation; enhance UI for server selection and interface naming, ensuring proper validation and error handling
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:
@@ -146,6 +146,7 @@ function NetworkConfigManager() {
|
|||||||
const [templateIpsecPasswordId, setTemplateIpsecPasswordId] = useState(''); // Выбранный IPSec пароль
|
const [templateIpsecPasswordId, setTemplateIpsecPasswordId] = useState(''); // Выбранный IPSec пароль
|
||||||
const [templateIpPoolId, setTemplateIpPoolId] = useState(''); // Выбранный IP пул
|
const [templateIpPoolId, setTemplateIpPoolId] = useState(''); // Выбранный IP пул
|
||||||
const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания
|
const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания
|
||||||
|
const [templateServerPairs, setTemplateServerPairs] = useState([]); // Массив пар серверов для множественного создания
|
||||||
|
|
||||||
// === IPSec Passwords Management ===
|
// === IPSec Passwords Management ===
|
||||||
const [ipsecPasswords, setIpsecPasswords] = useState([]);
|
const [ipsecPasswords, setIpsecPasswords] = useState([]);
|
||||||
@@ -511,41 +512,36 @@ function NetworkConfigManager() {
|
|||||||
const handleCreateInterfaceFromTemplate = () => {
|
const handleCreateInterfaceFromTemplate = () => {
|
||||||
const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100
|
const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100
|
||||||
|
|
||||||
// Если количество туннелей больше 1, создаем туннели между разными парами серверов
|
// Если количество туннелей больше 1, используем выбранные пары серверов
|
||||||
if (tunnelCount > 1) {
|
if (tunnelCount > 1) {
|
||||||
// Получаем список всех серверов
|
// Проверяем, что все пары заполнены
|
||||||
const availableServers = servers.filter(s => s.id && s.ip);
|
if (templateServerPairs.length !== tunnelCount) {
|
||||||
|
notify.error(`Необходимо настроить ${tunnelCount} пар серверов`);
|
||||||
if (availableServers.length < 2) {
|
|
||||||
notify.error('Недостаточно серверов для создания нескольких туннелей (нужно минимум 2)');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем все возможные пары серверов
|
const invalidPairs = templateServerPairs.filter((pair, index) => {
|
||||||
const serverPairs = [];
|
if (!pair.server1 || !pair.server2) {
|
||||||
for (let i = 0; i < availableServers.length; i++) {
|
return true;
|
||||||
for (let j = i + 1; j < availableServers.length; j++) {
|
|
||||||
serverPairs.push([availableServers[i].id, availableServers[j].id]);
|
|
||||||
}
|
}
|
||||||
}
|
if (pair.server1 === pair.server2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
if (serverPairs.length < tunnelCount) {
|
if (invalidPairs.length > 0) {
|
||||||
notify.error(`Недостаточно пар серверов для создания ${tunnelCount} туннелей. Доступно пар: ${serverPairs.length}`);
|
notify.error('Не все пары серверов настроены корректно. Проверьте, что выбраны разные серверы для каждой пары.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Берем первые N пар
|
|
||||||
const pairsToUse = serverPairs.slice(0, tunnelCount);
|
|
||||||
|
|
||||||
const newInterfaces = [];
|
const newInterfaces = [];
|
||||||
const usedIps = new Set(getAllUsedIpsSet);
|
const usedIps = new Set(getAllUsedIpsSet);
|
||||||
|
|
||||||
// Создаем туннели для каждой пары
|
// Создаем туннели для каждой пары
|
||||||
pairsToUse.forEach((pair, index) => {
|
templateServerPairs.forEach((pair, index) => {
|
||||||
const server1Id = pair[0];
|
const server1 = getServerInfo(pair.server1);
|
||||||
const server2Id = pair[1];
|
const server2 = getServerInfo(pair.server2);
|
||||||
const server1 = getServerInfo(server1Id);
|
|
||||||
const server2 = getServerInfo(server2Id);
|
|
||||||
|
|
||||||
if (!server1 || !server2) {
|
if (!server1 || !server2) {
|
||||||
notify.error(`Не удалось найти информацию о серверах для пары ${index + 1}`);
|
notify.error(`Не удалось найти информацию о серверах для пары ${index + 1}`);
|
||||||
@@ -555,8 +551,12 @@ function NetworkConfigManager() {
|
|||||||
// Генерируем имена интерфейсов
|
// Генерируем имена интерфейсов
|
||||||
const baseName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
|
const baseName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
|
||||||
const baseName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
|
const baseName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
|
||||||
const name1 = templateName1.trim() ? `${templateName1.trim()}-${index + 1}` : baseName1;
|
const name1 = pair.name1?.trim() || baseName1;
|
||||||
const name2 = templateName2.trim() ? `${templateName2.trim()}-${index + 1}` : baseName2;
|
const name2 = pair.name2?.trim() || baseName2;
|
||||||
|
|
||||||
|
// Используем настройки из пары или глобальные
|
||||||
|
const pairIpsecPasswordId = pair.ipsecPasswordId || templateIpsecPasswordId;
|
||||||
|
const pairIpPoolId = pair.ipPoolId || templateIpPoolId;
|
||||||
|
|
||||||
// Генерируем IP адреса (учитываем уже использованные в этой итерации)
|
// Генерируем IP адреса (учитываем уже использованные в этой итерации)
|
||||||
// Важно: Local IP и Remote IP должны быть разными и в одной сети /30
|
// Важно: Local IP и Remote IP должны быть разными и в одной сети /30
|
||||||
@@ -566,9 +566,9 @@ function NetworkConfigManager() {
|
|||||||
const maxAttempts = 1000;
|
const maxAttempts = 1000;
|
||||||
|
|
||||||
while ((!localIp || !remoteIp || localIp === remoteIp) && attempts < maxAttempts) {
|
while ((!localIp || !remoteIp || localIp === remoteIp) && attempts < maxAttempts) {
|
||||||
const candidateLocal = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
|
const candidateLocal = generateFreePrivateIp(null, false, null, pairIpPoolId || null);
|
||||||
if (candidateLocal && !usedIps.has(candidateLocal)) {
|
if (candidateLocal && !usedIps.has(candidateLocal)) {
|
||||||
const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, templateIpPoolId || null);
|
const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, pairIpPoolId || null);
|
||||||
if (candidateRemote && !usedIps.has(candidateRemote) && candidateRemote !== candidateLocal) {
|
if (candidateRemote && !usedIps.has(candidateRemote) && candidateRemote !== candidateLocal) {
|
||||||
localIp = candidateLocal;
|
localIp = candidateLocal;
|
||||||
remoteIp = candidateRemote;
|
remoteIp = candidateRemote;
|
||||||
@@ -593,10 +593,10 @@ function NetworkConfigManager() {
|
|||||||
type: templateType,
|
type: templateType,
|
||||||
localIp: localIp,
|
localIp: localIp,
|
||||||
remoteIp: remoteIp,
|
remoteIp: remoteIp,
|
||||||
serverId: server1Id,
|
serverId: pair.server1,
|
||||||
serverId2: server2Id,
|
serverId2: pair.server2,
|
||||||
ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && templateIpsecPasswordId ? templateIpsecPasswordId : '',
|
ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && pairIpsecPasswordId ? pairIpsecPasswordId : '',
|
||||||
ipPoolId: templateIpPoolId || ''
|
ipPoolId: pairIpPoolId || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Проверяем конфликты с существующими интерфейсами
|
// Проверяем конфликты с существующими интерфейсами
|
||||||
@@ -619,7 +619,7 @@ function NetworkConfigManager() {
|
|||||||
tunnelInterfaces: [...prev.tunnelInterfaces, ...newInterfaces],
|
tunnelInterfaces: [...prev.tunnelInterfaces, ...newInterfaces],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
notify.success(`Создано туннелей: ${newInterfaces.length} между разными парами серверов`);
|
notify.success(`Создано туннелей: ${newInterfaces.length}`);
|
||||||
|
|
||||||
// Закрываем модал и сбрасываем значения
|
// Закрываем модал и сбрасываем значения
|
||||||
setInterfaceTemplateModalOpen(false);
|
setInterfaceTemplateModalOpen(false);
|
||||||
@@ -632,6 +632,7 @@ function NetworkConfigManager() {
|
|||||||
setTemplateIpsecPasswordId('');
|
setTemplateIpsecPasswordId('');
|
||||||
setTemplateIpPoolId('');
|
setTemplateIpPoolId('');
|
||||||
setTemplateTunnelCount(1);
|
setTemplateTunnelCount(1);
|
||||||
|
setTemplateServerPairs([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3297,6 +3298,7 @@ function NetworkConfigManager() {
|
|||||||
setTemplateIpsecPasswordId('');
|
setTemplateIpsecPasswordId('');
|
||||||
setTemplateIpPoolId('');
|
setTemplateIpPoolId('');
|
||||||
setTemplateTunnelCount(1);
|
setTemplateTunnelCount(1);
|
||||||
|
setTemplateServerPairs([]);
|
||||||
}}
|
}}
|
||||||
onSubmit={handleCreateInterfaceFromTemplate}
|
onSubmit={handleCreateInterfaceFromTemplate}
|
||||||
title="Создать интерфейс из шаблона"
|
title="Создать интерфейс из шаблона"
|
||||||
@@ -3313,14 +3315,32 @@ function NetworkConfigManager() {
|
|||||||
value={templateTunnelCount}
|
value={templateTunnelCount}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = parseInt(e.target.value) || 1;
|
const value = parseInt(e.target.value) || 1;
|
||||||
setTemplateTunnelCount(Math.max(1, Math.min(100, value)));
|
const newCount = Math.max(1, Math.min(100, value));
|
||||||
|
setTemplateTunnelCount(newCount);
|
||||||
|
// Синхронизируем количество пар
|
||||||
|
if (newCount > templateServerPairs.length) {
|
||||||
|
const newPairs = [...templateServerPairs];
|
||||||
|
while (newPairs.length < newCount) {
|
||||||
|
newPairs.push({
|
||||||
|
server1: '',
|
||||||
|
server2: '',
|
||||||
|
name1: '',
|
||||||
|
name2: '',
|
||||||
|
ipsecPasswordId: '',
|
||||||
|
ipPoolId: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTemplateServerPairs(newPairs);
|
||||||
|
} else if (newCount < templateServerPairs.length) {
|
||||||
|
setTemplateServerPairs(templateServerPairs.slice(0, newCount));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
/>
|
/>
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
{templateTunnelCount > 1
|
{templateTunnelCount > 1
|
||||||
? `Будет создано ${templateTunnelCount} туннелей между разными парами серверов (каждый с каждым)`
|
? `Настройте ${templateTunnelCount} пар серверов ниже`
|
||||||
: 'Сколько туннелей создать между выбранными серверами (1-100)'}
|
: 'Сколько туннелей создать между выбранными серверами (1-100)'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3345,7 +3365,7 @@ function NetworkConfigManager() {
|
|||||||
/>
|
/>
|
||||||
<div className="form-text">Будет добавлен перед именем интерфейса</div>
|
<div className="form-text">Будет добавлен перед именем интерфейса</div>
|
||||||
</div>
|
</div>
|
||||||
{templateTunnelCount === 1 && (
|
{(templateTunnelCount === 1) && (
|
||||||
<>
|
<>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label required">Сервер 1</label>
|
<label className="form-label required">Сервер 1</label>
|
||||||
@@ -3387,15 +3407,274 @@ function NetworkConfigManager() {
|
|||||||
)}
|
)}
|
||||||
{templateTunnelCount > 1 && (
|
{templateTunnelCount > 1 && (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="alert alert-info">
|
<label className="form-label required">Пары серверов</label>
|
||||||
<strong>Режим множественного создания:</strong> Будет создано {templateTunnelCount} туннелей между разными парами серверов.
|
<div className="d-flex flex-column gap-3">
|
||||||
<br />
|
{Array.from({ length: templateTunnelCount }).map((_, index) => {
|
||||||
Туннели будут созданы между всеми возможными парами серверов (каждый с каждым).
|
const pair = templateServerPairs[index] || {
|
||||||
<br />
|
server1: '',
|
||||||
<small>Доступно серверов: {servers.filter(s => s.id && s.ip).length}. Максимум пар: {Math.floor((servers.filter(s => s.id && s.ip).length * (servers.filter(s => s.id && s.ip).length - 1)) / 2)}</small>
|
server2: '',
|
||||||
|
name1: '',
|
||||||
|
name2: '',
|
||||||
|
ipsecPasswordId: '',
|
||||||
|
ipPoolId: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePair = (updates) => {
|
||||||
|
const newPairs = [...templateServerPairs];
|
||||||
|
while (newPairs.length < templateTunnelCount) {
|
||||||
|
newPairs.push({
|
||||||
|
server1: '',
|
||||||
|
server2: '',
|
||||||
|
name1: '',
|
||||||
|
name2: '',
|
||||||
|
ipsecPasswordId: '',
|
||||||
|
ipPoolId: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
newPairs[index] = { ...pair, ...updates };
|
||||||
|
setTemplateServerPairs(newPairs.slice(0, templateTunnelCount));
|
||||||
|
};
|
||||||
|
|
||||||
|
const server1 = getServerInfo(pair.server1);
|
||||||
|
const server2 = getServerInfo(pair.server2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="card border">
|
||||||
|
<div className="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<span className="fw-semibold">Туннель {index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label required">Сервер 1</label>
|
||||||
|
<ServerAutocompleteInput
|
||||||
|
value={pair.server1}
|
||||||
|
onChange={(val) => updatePair({ server1: val })}
|
||||||
|
servers={servers.filter(s => s.id !== pair.server2 && s.ip !== pair.server2)}
|
||||||
|
placeholder="Выберите первый сервер..."
|
||||||
|
/>
|
||||||
|
{server1 && (
|
||||||
|
<div className="form-text">
|
||||||
|
{server1.dns || server1.ip}
|
||||||
|
{server1.country && ` • ${countryToFlag(server1.country)} ${server1.country}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label required">Сервер 2</label>
|
||||||
|
<ServerAutocompleteInput
|
||||||
|
value={pair.server2}
|
||||||
|
onChange={(val) => updatePair({ server2: val })}
|
||||||
|
servers={servers.filter(s => s.id !== pair.server1 && s.ip !== pair.server1)}
|
||||||
|
placeholder="Выберите второй сервер..."
|
||||||
|
/>
|
||||||
|
{server2 && (
|
||||||
|
<div className="form-text">
|
||||||
|
{server2.dns || server2.ip}
|
||||||
|
{server2.country && ` • ${countryToFlag(server2.country)} ${server2.country}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Имя интерфейса (сервер 1)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={pair.name1 || ''}
|
||||||
|
onChange={(e) => updatePair({ name1: e.target.value })}
|
||||||
|
placeholder={server1 && server2 ? generateInterfaceName(server1, server2, templateType, templateNamePrefix) : ''}
|
||||||
|
/>
|
||||||
|
<div className="form-text">Оставьте пустым для автогенерации</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Имя интерфейса (сервер 2)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={pair.name2 || ''}
|
||||||
|
onChange={(e) => updatePair({ name2: e.target.value })}
|
||||||
|
placeholder={server2 && server1 ? generateInterfaceName(server2, server1, templateType, templateNamePrefix) : ''}
|
||||||
|
/>
|
||||||
|
<div className="form-text">Оставьте пустым для автогенерации</div>
|
||||||
|
</div>
|
||||||
|
{(templateType === 'IPSec' || templateType === 'GRE') && (
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">IPSec пароль (опционально)</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={pair.ipsecPasswordId || ''}
|
||||||
|
onChange={(e) => updatePair({ ipsecPasswordId: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Использовать глобальный ({templateIpsecPasswordId ? ipsecPasswords.find(p => p.id === templateIpsecPasswordId)?.name || 'выбран' : 'не выбран'})</option>
|
||||||
|
{ipsecPasswords.map(pwd => (
|
||||||
|
<option key={pwd.id} value={pwd.id}>
|
||||||
|
{pwd.name} {pwd.description ? `(${pwd.description})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">IP пул (опционально)</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={pair.ipPoolId || ''}
|
||||||
|
onChange={(e) => updatePair({ ipPoolId: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Использовать глобальный ({templateIpPoolId ? config.ipPools?.find(p => p.id === templateIpPoolId)?.name || 'выбран' : 'автоматический'})</option>
|
||||||
|
{config.ipPools?.map(pool => (
|
||||||
|
<option key={pool.id} value={pool.id}>
|
||||||
|
{pool.name} ({pool.cidr}) {pool.description ? `- ${pool.description}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{(templateTunnelCount === 1) && (
|
||||||
|
<>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Имя интерфейса (сервер 1)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={templateName1}
|
||||||
|
onChange={(e) => setTemplateName1(e.target.value)}
|
||||||
|
placeholder={templateServer1 && templateServer2 ? (() => {
|
||||||
|
const s1 = getServerInfo(templateServer1);
|
||||||
|
const s2 = getServerInfo(templateServer2);
|
||||||
|
return s1 && s2 ? generateInterfaceName(s1, s2, templateType, templateNamePrefix) : '';
|
||||||
|
})() : ''}
|
||||||
|
/>
|
||||||
|
<div className="form-text">Оставьте пустым для автогенерации</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Имя интерфейса (сервер 2)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={templateName2}
|
||||||
|
onChange={(e) => setTemplateName2(e.target.value)}
|
||||||
|
placeholder={templateServer1 && templateServer2 ? (() => {
|
||||||
|
const s1 = getServerInfo(templateServer1);
|
||||||
|
const s2 = getServerInfo(templateServer2);
|
||||||
|
return s1 && s2 ? generateInterfaceName(s2, s1, templateType, templateNamePrefix) : '';
|
||||||
|
})() : ''}
|
||||||
|
/>
|
||||||
|
<div className="form-text">Оставьте пустым для автогенерации</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(templateType === 'IPSec' || templateType === 'GRE') && (
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label">IPSec пароль (глобальный, используется если не указан для пары)</label>
|
||||||
|
<div className="input-group">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={templateIpsecPasswordId}
|
||||||
|
onChange={(e) => setTemplateIpsecPasswordId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Не выбран</option>
|
||||||
|
{ipsecPasswords.map(pwd => (
|
||||||
|
<option key={pwd.id} value={pwd.id}>
|
||||||
|
{pwd.name} {pwd.description ? `(${pwd.description})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setIpsecPasswordModalMode('add');
|
||||||
|
setEditingIpsecPassword({ name: '', password: '', description: '' });
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
}}
|
||||||
|
title="Создать новый IPSec пароль"
|
||||||
|
>
|
||||||
|
<IconPlus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-text">
|
||||||
|
Выберите сохраненный IPSec пароль или создайте новый (опционально для GRE). Будет использован для всех туннелей, если не указан индивидуально.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label">IP пул (глобальный, используется если не указан для пары)</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={templateIpPoolId}
|
||||||
|
onChange={(e) => setTemplateIpPoolId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Автоматический подбор (RFC 1918)</option>
|
||||||
|
{config.ipPools?.map(pool => (
|
||||||
|
<option key={pool.id} value={pool.id}>
|
||||||
|
{pool.name} ({pool.cidr}) {pool.description ? `- ${pool.description}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="form-text">
|
||||||
|
Выберите IP пул для подбора адресов или оставьте автоматический подбор. Будет использован для всех туннелей, если не указан индивидуально.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{templateTunnelCount === 1 && templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => {
|
||||||
|
const server1 = getServerInfo(templateServer1);
|
||||||
|
const server2 = getServerInfo(templateServer2);
|
||||||
|
if (!server1 || !server2) return null;
|
||||||
|
|
||||||
|
const autoName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
|
||||||
|
const autoName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
|
||||||
|
const previewName1 = templateName1.trim() || autoName1;
|
||||||
|
const previewName2 = templateName2.trim() || autoName2;
|
||||||
|
const previewLocalIp = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
|
||||||
|
const previewRemoteIp = previewLocalIp ? generateFreePrivateIp(null, true, previewLocalIp, templateIpPoolId || null) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="card card-sm border">
|
||||||
|
<div className="card-header bg-light">
|
||||||
|
<strong>Предпросмотр интерфейса:</strong>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="mb-2">
|
||||||
|
<strong>Сервер 1 ({server1.dns || server1.ip}):</strong>
|
||||||
|
<div className="ms-2">
|
||||||
|
<div>Имя: {previewName1}</div>
|
||||||
|
<div>Local IP: {previewLocalIp || '—'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="mb-2">
|
||||||
|
<strong>Сервер 2 ({server2.dns || server2.ip}):</strong>
|
||||||
|
<div className="ms-2">
|
||||||
|
<div>Имя: {previewName2}</div>
|
||||||
|
<div>Local IP: {previewRemoteIp || '—'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{previewLocalIp && previewRemoteIp && (
|
||||||
|
<div className="col-12 text-center">
|
||||||
|
<IconArrowsRightLeft size={20} className="text-muted" />
|
||||||
|
<span className="ms-2 text-muted">{previewLocalIp} ↔ {previewRemoteIp}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => {
|
{templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => {
|
||||||
const server1 = getServerInfo(templateServer1);
|
const server1 = getServerInfo(templateServer1);
|
||||||
const server2 = getServerInfo(templateServer2);
|
const server2 = getServerInfo(templateServer2);
|
||||||
|
|||||||
Reference in New Issue
Block a user