feat(NetworkConfigManager): add support for dual server configurations in network interfaces; enhance MikroTik code generation and UI to display both servers
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -57,8 +57,8 @@
|
||||
"type": "GRE",
|
||||
"localIp": "10.10.0.1",
|
||||
"remoteIp": "10.10.0.2",
|
||||
"port": "",
|
||||
"serverId": "SWE-HIPHOST"
|
||||
"serverId": "SWE-HIPHOST",
|
||||
"serverId2": "DE-FRANKFURT"
|
||||
},
|
||||
{
|
||||
"id": "if-wg-swe-nl",
|
||||
@@ -66,7 +66,8 @@
|
||||
"type": "WireGuard",
|
||||
"localIp": "10.20.0.1",
|
||||
"remoteIp": "10.20.0.2",
|
||||
"serverId": "SWE-HIPHOST"
|
||||
"serverId": "SWE-HIPHOST",
|
||||
"serverId2": "NL-AMSTERDAM"
|
||||
},
|
||||
{
|
||||
"id": "if-gre-de-us",
|
||||
@@ -74,8 +75,8 @@
|
||||
"type": "GRE",
|
||||
"localIp": "10.10.1.1",
|
||||
"remoteIp": "10.10.1.2",
|
||||
"port": "",
|
||||
"serverId": "DE-FRANKFURT"
|
||||
"serverId": "DE-FRANKFURT",
|
||||
"serverId2": "US-NEWYORK"
|
||||
},
|
||||
{
|
||||
"id": "if-ipsec-nl-jp",
|
||||
@@ -83,7 +84,6 @@
|
||||
"type": "IPSec",
|
||||
"localIp": "10.30.0.1",
|
||||
"remoteIp": "10.30.0.2",
|
||||
"port": "",
|
||||
"serverId": "NL-AMSTERDAM"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -77,6 +77,7 @@ const getEmptyInterface = () => ({
|
||||
localIp: '',
|
||||
remoteIp: '',
|
||||
serverId: '',
|
||||
serverId2: '', // Второй сервер (опциональный)
|
||||
});
|
||||
|
||||
const getEmptyIpPool = () => ({
|
||||
@@ -298,9 +299,17 @@ function NetworkConfigManager() {
|
||||
const interfacesByServer = useMemo(() => {
|
||||
const grouped = {};
|
||||
filteredInterfaces.forEach(iface => {
|
||||
const key = iface.serverId || '__unassigned__';
|
||||
if (!grouped[key]) grouped[key] = [];
|
||||
grouped[key].push(iface);
|
||||
// Группируем по первому серверу
|
||||
const key1 = iface.serverId || '__unassigned__';
|
||||
if (!grouped[key1]) grouped[key1] = [];
|
||||
grouped[key1].push(iface);
|
||||
|
||||
// Если есть второй сервер, также добавляем в его группу
|
||||
if (iface.serverId2) {
|
||||
const key2 = iface.serverId2;
|
||||
if (!grouped[key2]) grouped[key2] = [];
|
||||
grouped[key2].push(iface);
|
||||
}
|
||||
});
|
||||
return grouped;
|
||||
}, [filteredInterfaces]);
|
||||
@@ -492,6 +501,70 @@ function NetworkConfigManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// === Генерация кода MikroTik для настройки IP адресов интерфейсов ===
|
||||
const generateMikrotikInterfaceAddresses = () => {
|
||||
const interfacesWithBothServers = (config.tunnelInterfaces || []).filter(i =>
|
||||
i.serverId && i.serverId2 && i.localIp && i.remoteIp
|
||||
);
|
||||
|
||||
if (interfacesWithBothServers.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let code = '\n# === Настройка IP адресов интерфейсов на обоих серверах ===\n';
|
||||
code += '# Эти команды нужно выполнить на соответствующих серверах\n\n';
|
||||
|
||||
// Группируем по серверам
|
||||
const interfacesByServer = {};
|
||||
interfacesWithBothServers.forEach(iface => {
|
||||
const server1 = getServerInfo(iface.serverId);
|
||||
const server2 = getServerInfo(iface.serverId2);
|
||||
|
||||
if (!server1 || !server2) return;
|
||||
|
||||
const server1Name = server1.dns || server1.ip || iface.serverId;
|
||||
const server2Name = server2.dns || server2.ip || iface.serverId2;
|
||||
const interfaceName = iface.name || `${iface.type}-tunnel`;
|
||||
|
||||
// Для первого сервера - используем localIp
|
||||
if (!interfacesByServer[server1Name]) {
|
||||
interfacesByServer[server1Name] = [];
|
||||
}
|
||||
interfacesByServer[server1Name].push({
|
||||
interfaceName,
|
||||
ip: iface.localIp,
|
||||
type: iface.type,
|
||||
remoteIp: iface.remoteIp,
|
||||
server2Name
|
||||
});
|
||||
|
||||
// Для второго сервера - используем remoteIp
|
||||
if (!interfacesByServer[server2Name]) {
|
||||
interfacesByServer[server2Name] = [];
|
||||
}
|
||||
interfacesByServer[server2Name].push({
|
||||
interfaceName,
|
||||
ip: iface.remoteIp,
|
||||
type: iface.type,
|
||||
remoteIp: iface.localIp, // Для второго сервера remoteIp - это localIp первого
|
||||
server2Name: server1Name
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(interfacesByServer).forEach(([serverName, interfaces]) => {
|
||||
code += `\n# --- Команды для сервера: ${serverName} ---\n`;
|
||||
interfaces.forEach(iface => {
|
||||
code += `# Интерфейс: ${iface.interfaceName} (${iface.type}) - связь с ${iface.server2Name}\n`;
|
||||
code += `/ip address add address=${iface.ip}/32 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`;
|
||||
});
|
||||
});
|
||||
|
||||
code += '\n# Проверка настроенных адресов:\n';
|
||||
code += '# /ip address print\n';
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
// === Генерация кода MikroTik для рекурсивных маршрутов ===
|
||||
const generateMikrotikRecursiveRoutes = () => {
|
||||
const recursiveGateways = (config.gateways || []).filter(gw => gw.type === 'recursive');
|
||||
@@ -575,6 +648,12 @@ function NetworkConfigManager() {
|
||||
code += '# /ip route print where comment~"Recursive"\n';
|
||||
code += `\n# Всего создано маршрутов: ${validRoutesCount}\n`;
|
||||
|
||||
// Добавляем код для настройки IP адресов интерфейсов
|
||||
const interfaceAddressesCode = generateMikrotikInterfaceAddresses();
|
||||
if (interfaceAddressesCode) {
|
||||
code += interfaceAddressesCode;
|
||||
}
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
@@ -813,73 +892,96 @@ function NetworkConfigManager() {
|
||||
};
|
||||
|
||||
// === Рендер карточки Interface ===
|
||||
const renderInterfaceCard = (iface) => (
|
||||
<div key={iface.id} className="col-sm-6 col-lg-4">
|
||||
<div className="card card-sm">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<span className={`avatar avatar-sm bg-${getInterfaceTypeColor(iface.type)}-lt me-2`}>
|
||||
<IconRouter size={16} className={`text-${getInterfaceTypeColor(iface.type)}`} />
|
||||
</span>
|
||||
<div className="flex-fill">
|
||||
<div className="fw-medium">{iface.name || '—'}</div>
|
||||
const renderInterfaceCard = (iface) => {
|
||||
const server = getServerInfo(iface.serverId);
|
||||
const server2 = getServerInfo(iface.serverId2);
|
||||
|
||||
return (
|
||||
<div key={iface.id} className="col-sm-6 col-lg-4">
|
||||
<div className="card card-sm">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<span className={`avatar avatar-sm bg-${getInterfaceTypeColor(iface.type)}-lt me-2`}>
|
||||
<IconRouter size={16} className={`text-${getInterfaceTypeColor(iface.type)}`} />
|
||||
</span>
|
||||
<div className="flex-fill">
|
||||
<div className="fw-medium">{iface.name || '—'}</div>
|
||||
<div className="text-muted small">
|
||||
<span className={`badge bg-${getInterfaceTypeColor(iface.type)}-lt text-${getInterfaceTypeColor(iface.type)}`}>
|
||||
{iface.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropdown">
|
||||
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
<div className="dropdown-menu dropdown-menu-end">
|
||||
<button className="dropdown-item" onClick={() => handleEditInterface(iface)}>
|
||||
<IconEdit size={16} className="me-2" />Редактировать
|
||||
</button>
|
||||
<button className="dropdown-item text-danger" onClick={() => handleDeleteInterface(iface)}>
|
||||
<IconTrash size={16} className="me-2" />Удалить
|
||||
</div>
|
||||
<div className="dropdown">
|
||||
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
<div className="dropdown-menu dropdown-menu-end">
|
||||
<button className="dropdown-item" onClick={() => handleEditInterface(iface)}>
|
||||
<IconEdit size={16} className="me-2" />Редактировать
|
||||
</button>
|
||||
<button className="dropdown-item text-danger" onClick={() => handleDeleteInterface(iface)}>
|
||||
<IconTrash size={16} className="me-2" />Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row g-2 small">
|
||||
<div className="col-6">
|
||||
<div className="text-muted">Local IP</div>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<code className="text-truncate">{iface.localIp || '—'}</code>
|
||||
{iface.localIp && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(iface.localIp)}
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="row g-2 small mb-2">
|
||||
<div className="col-6">
|
||||
<div className="text-muted">Local IP</div>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<code className="text-truncate">{iface.localIp || '—'}</code>
|
||||
{iface.localIp && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(iface.localIp)}
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-muted">Remote IP</div>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<code className="text-truncate">{iface.remoteIp || '—'}</code>
|
||||
{iface.remoteIp && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(iface.remoteIp)}
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-muted">Remote IP</div>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<code className="text-truncate">{iface.remoteIp || '—'}</code>
|
||||
{iface.remoteIp && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(iface.remoteIp)}
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Серверы */}
|
||||
<div className="small">
|
||||
{server && (
|
||||
<div className="d-flex align-items-center gap-1 mb-1">
|
||||
<IconServer size={12} className="text-muted" />
|
||||
<span className="text-muted">Сервер 1:</span>
|
||||
<span>{server.dns || server.ip}</span>
|
||||
</div>
|
||||
)}
|
||||
{server2 && (
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<IconServer size={12} className="text-muted" />
|
||||
<span className="text-muted">Сервер 2:</span>
|
||||
<span>{server2.dns || server2.ip}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="network-config-manager">
|
||||
@@ -1307,13 +1409,14 @@ function NetworkConfigManager() {
|
||||
<th>Тип</th>
|
||||
<th>Local IP</th>
|
||||
<th>Remote IP</th>
|
||||
<th>Сервер</th>
|
||||
<th>Серверы</th>
|
||||
<th className="text-end" style={{ width: 100 }}>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredInterfaces.map(iface => {
|
||||
const server = getServerInfo(iface.serverId);
|
||||
const server2 = getServerInfo(iface.serverId2);
|
||||
return (
|
||||
<tr key={iface.id}>
|
||||
<td><strong>{iface.name || '—'}</strong></td>
|
||||
@@ -1336,18 +1439,39 @@ function NetworkConfigManager() {
|
||||
)}
|
||||
</code>
|
||||
</td>
|
||||
<td><code>{iface.remoteIp || '—'}</code></td>
|
||||
<td>
|
||||
{server ? (
|
||||
<div className="d-flex align-items-center">
|
||||
<IconServer size={14} className="text-muted me-1" />
|
||||
<span>{server.dns || server.ip}</span>
|
||||
</div>
|
||||
) : iface.serverId ? (
|
||||
<span className="text-muted">{iface.serverId}</span>
|
||||
) : (
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
<code className="d-flex align-items-center gap-1">
|
||||
{iface.remoteIp || '—'}
|
||||
{iface.remoteIp && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(iface.remoteIp)}
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
)}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{server ? (
|
||||
<div className="d-flex align-items-center">
|
||||
<IconServer size={14} className="text-muted me-1" />
|
||||
<span>{server.dns || server.ip}</span>
|
||||
</div>
|
||||
) : iface.serverId ? (
|
||||
<span className="text-muted">{iface.serverId}</span>
|
||||
) : (
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
{server2 && (
|
||||
<div className="d-flex align-items-center mt-1">
|
||||
<IconServer size={14} className="text-muted me-1" />
|
||||
<span className="text-muted small">{server2.dns || server2.ip}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<div className="btn-list gap-1 mb-0 justify-content-end">
|
||||
@@ -1617,7 +1741,7 @@ function NetworkConfigManager() {
|
||||
placeholder="10.10.0.1"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="col-md-4">
|
||||
<FormField
|
||||
label="Remote IP"
|
||||
name="remoteIp"
|
||||
@@ -1626,6 +1750,52 @@ function NetworkConfigManager() {
|
||||
placeholder="10.10.0.2"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Второй сервер (опционально)</label>
|
||||
<ServerAutocompleteInput
|
||||
value={editingInterface.serverId2 || ''}
|
||||
onChange={(val) => setEditingInterface({ ...editingInterface, serverId2: val })}
|
||||
servers={servers.filter(s => s.id !== editingInterface.serverId && s.ip !== editingInterface.serverId)}
|
||||
placeholder="Выберите второй сервер (для понимания связи между серверами)..."
|
||||
/>
|
||||
<div className="form-text">
|
||||
Укажите второй сервер, если интерфейс связывает два сервера
|
||||
</div>
|
||||
</div>
|
||||
{editingInterface.serverId && (() => {
|
||||
const server = getServerInfo(editingInterface.serverId);
|
||||
return server ? (
|
||||
<div className="col-12">
|
||||
<div className="alert alert-info mb-0">
|
||||
<div className="d-flex align-items-center">
|
||||
<IconServer size={16} className="me-2" />
|
||||
<div>
|
||||
<strong>Сервер 1:</strong> {server.dns || server.ip}
|
||||
{server.provider && <span className="ms-2">• {server.provider}</span>}
|
||||
{server.country && <span className="ms-2">• {countryToFlag(server.country)} {server.country}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
{editingInterface.serverId2 && (() => {
|
||||
const server2 = getServerInfo(editingInterface.serverId2);
|
||||
return server2 ? (
|
||||
<div className="col-12">
|
||||
<div className="alert alert-info mb-0">
|
||||
<div className="d-flex align-items-center">
|
||||
<IconServer size={16} className="me-2" />
|
||||
<div>
|
||||
<strong>Сервер 2:</strong> {server2.dns || server2.ip}
|
||||
{server2.provider && <span className="ms-2">• {server2.provider}</span>}
|
||||
{server2.country && <span className="ms-2">• {countryToFlag(server2.country)} {server2.country}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
Reference in New Issue
Block a user