refactor(NetworkConfigManager): enhance gateway and interface card rendering with improved layout and indicator display; update MikroTik code generation for gateway values
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -671,17 +671,22 @@ function NetworkConfigManager() {
|
||||
code += `# Рекурсивный gateway: ${gw.ip}\n`;
|
||||
code += `# Родительский: ${parentLabel}\n`;
|
||||
|
||||
// Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP
|
||||
const gatewayValue = parent.parentType === 'interface'
|
||||
? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}`
|
||||
: parentIp;
|
||||
|
||||
// Создаём маршрут к IP рекурсивного gateway через родительский
|
||||
const comment = gw.description
|
||||
? `${gw.description} -> ${parentIp}`
|
||||
: `Recursive: ${gw.ip} -> ${parentIp}`;
|
||||
code += `/ip route add dst-address=${gw.ip}/32 gateway=${parentIp} comment="${comment}"\n`;
|
||||
code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue} 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\n`;
|
||||
const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`;
|
||||
code += `/ip route add dst-address=0.0.0.0/0 gateway=${parentIp} comment="${defaultComment}"\n`;
|
||||
code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue} comment="${defaultComment}"\n`;
|
||||
}
|
||||
code += '\n';
|
||||
});
|
||||
@@ -860,7 +865,7 @@ function NetworkConfigManager() {
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div className="row row-cards">
|
||||
<div className="d-flex flex-column">
|
||||
{items.map(item => (
|
||||
type === 'gateway' ? renderGatewayCard(item) : renderInterfaceCard(item, serverId)
|
||||
))}
|
||||
@@ -917,79 +922,87 @@ function NetworkConfigManager() {
|
||||
? getParentGateway(gateway.parentGatewayId)
|
||||
: null;
|
||||
|
||||
// Определяем тип индикатора
|
||||
const indicatorType = gateway.type === 'recursive' ? 'EXIT' : 'JUMP';
|
||||
const indicatorColor = gateway.type === 'recursive' ? 'red' : 'blue';
|
||||
const indicatorIcon = gateway.type === 'recursive' ? IconRouter : IconWorld;
|
||||
|
||||
// Определяем отображаемый IP/домен для зеленого блока
|
||||
const displayIp = parentGateway
|
||||
? (parentGateway.parentType === 'interface'
|
||||
? (parentGateway.name || parentGateway.interfaceType || 'interface')
|
||||
: parentGateway.ip)
|
||||
: gateway.ip;
|
||||
|
||||
return (
|
||||
<div key={gateway.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-${gatewayType.color}-lt me-2`}>
|
||||
<IconWorld size={16} className={`text-${gatewayType.color}`} />
|
||||
</span>
|
||||
<div className="flex-fill">
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<code className="fw-medium">{gateway.ip || '—'}</code>
|
||||
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`} style={{ fontSize: '0.65rem' }}>
|
||||
{gatewayType.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted small">
|
||||
{gateway.description || provider || '—'}
|
||||
</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={() => handleEditGateway(gateway)}>
|
||||
<IconEdit size={16} className="me-2" />Редактировать
|
||||
</button>
|
||||
<button className="dropdown-item text-danger" onClick={() => handleDeleteGateway(gateway)}>
|
||||
<IconTrash size={16} className="me-2" />Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mb-2">
|
||||
<code className="flex-fill text-truncate" title={gateway.ip}>
|
||||
{gateway.ip || '—'}
|
||||
</code>
|
||||
{gateway.ip && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon btn-sm"
|
||||
onClick={() => copyToClipboard(gateway.ip)}
|
||||
title="Копировать IP"
|
||||
>
|
||||
<IconCopy size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayCountry && (
|
||||
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
|
||||
<span>{countryToFlag(displayCountry)}</span>
|
||||
<span>{displayCountry}</span>
|
||||
{gatewayCountry && serverCountry && gatewayCountry !== serverCountry && (
|
||||
<span className="text-muted" style={{ fontSize: '0.7rem' }}>(сервер: {serverCountry})</span>
|
||||
<div key={gateway.id} className="card mb-2">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center">
|
||||
{/* Индикатор типа */}
|
||||
<div className={`d-flex flex-column align-items-center me-3`} style={{ minWidth: '60px' }}>
|
||||
<div className={`avatar avatar-md bg-${indicatorColor}-lt mb-1`}>
|
||||
{indicatorType === 'EXIT' ? (
|
||||
<IconRouter size={20} className={`text-${indicatorColor}`} />
|
||||
) : (
|
||||
<IconWorld size={20} className={`text-${indicatorColor}`} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={`text-${indicatorColor} fw-bold`} style={{ fontSize: '0.7rem' }}>{indicatorType}</span>
|
||||
</div>
|
||||
|
||||
{parentGateway && (
|
||||
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
|
||||
<span className="text-muted">→</span>
|
||||
<span>
|
||||
Родитель: {parentGateway.parentType === 'interface'
|
||||
? `Интерфейс ${parentGateway.name || parentGateway.interfaceType || 'interface'} (${parentGateway.remoteIp || '—'})`
|
||||
: parentGateway.ip
|
||||
? `Gateway ${parentGateway.ip}`
|
||||
: 'Gateway (не указан IP)'
|
||||
}
|
||||
</span>
|
||||
{/* Флаг и название */}
|
||||
<div className="d-flex align-items-center me-3" style={{ minWidth: '200px' }}>
|
||||
{displayCountry && (
|
||||
<span className="me-2" style={{ fontSize: '1.2rem' }}>{countryToFlag(displayCountry)}</span>
|
||||
)}
|
||||
<div>
|
||||
<div className="fw-bold">{gateway.description || provider || gateway.ip || '—'}</div>
|
||||
{server && (
|
||||
<div className="text-muted small">{server.dns || server.ip || gateway.serverId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Теги */}
|
||||
<div className="d-flex align-items-center gap-2 me-3">
|
||||
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`}>
|
||||
{gatewayType.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Зеленый блок с IP/доменом */}
|
||||
{displayIp && (
|
||||
<div className="bg-success-lt rounded px-3 py-2 me-3 d-flex align-items-center">
|
||||
<span className="text-success me-1">→</span>
|
||||
<code className="text-success fw-medium">{displayIp}</code>
|
||||
<button
|
||||
className="btn btn-ghost-success btn-icon btn-sm ms-2"
|
||||
onClick={() => copyToClipboard(displayIp)}
|
||||
title="Копировать"
|
||||
style={{ padding: '2px', width: '20px', height: '20px' }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопки действий */}
|
||||
<div className="ms-auto d-flex align-items-center gap-2">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => handleEditGateway(gateway)}
|
||||
>
|
||||
<IconEdit size={16} className="me-1" />
|
||||
Изменить
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => handleDeleteGateway(gateway)}
|
||||
>
|
||||
<IconTrash size={16} className="me-1" />
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1002,12 +1015,10 @@ function NetworkConfigManager() {
|
||||
const server2 = getServerInfo(iface.serverId2);
|
||||
|
||||
// Определяем, для какого сервера отображается карточка
|
||||
// Сравниваем currentServerId с serverId2 используя функцию getServerInfo для надежности
|
||||
const isForServer2 = currentServerId && iface.serverId2 && (() => {
|
||||
const currentServer = getServerInfo(currentServerId);
|
||||
const targetServer2 = getServerInfo(iface.serverId2);
|
||||
if (!currentServer || !targetServer2) return false;
|
||||
// Сравниваем по id, ip или dns
|
||||
return (
|
||||
currentServer.id === targetServer2.id ||
|
||||
currentServer.ip === targetServer2.ip ||
|
||||
@@ -1025,94 +1036,82 @@ function NetworkConfigManager() {
|
||||
: (iface.name || '—');
|
||||
|
||||
// IP адреса также зависят от того, для какого сервера отображается
|
||||
// Для второго сервера localIp и remoteIp меняются местами
|
||||
const displayLocalIp = isForServer2 ? (iface.remoteIp || '—') : (iface.localIp || '—');
|
||||
const displayRemoteIp = isForServer2 ? (iface.localIp || '—') : (iface.remoteIp || '—');
|
||||
|
||||
// Определяем сервер для отображения
|
||||
const displayServer = isForServer2 ? server2 : server;
|
||||
const displayServerName = displayServer?.dns || displayServer?.ip || (isForServer2 ? iface.serverId2 : iface.serverId);
|
||||
const displayCountry = displayServer?.country || '';
|
||||
const interfaceTypeColor = getInterfaceTypeColor(iface.type);
|
||||
|
||||
// Определяем отображаемый IP для зеленого блока (remote IP другого сервера)
|
||||
const displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp;
|
||||
|
||||
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)}`} />
|
||||
<div key={iface.id} className="card mb-2">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center">
|
||||
{/* Индикатор типа */}
|
||||
<div className={`d-flex flex-column align-items-center me-3`} style={{ minWidth: '60px' }}>
|
||||
<div className={`avatar avatar-md bg-${interfaceTypeColor}-lt mb-1`}>
|
||||
<IconRouter size={20} className={`text-${interfaceTypeColor}`} />
|
||||
</div>
|
||||
<span className={`text-${interfaceTypeColor} fw-bold`} style={{ fontSize: '0.7rem' }}>{iface.type}</span>
|
||||
</div>
|
||||
|
||||
{/* Флаг и название */}
|
||||
<div className="d-flex align-items-center me-3" style={{ minWidth: '200px' }}>
|
||||
{displayCountry && (
|
||||
<span className="me-2" style={{ fontSize: '1.2rem' }}>{countryToFlag(displayCountry)}</span>
|
||||
)}
|
||||
<div>
|
||||
<div className="fw-bold">{interfaceName}</div>
|
||||
{displayServerName && (
|
||||
<div className="text-muted small">{displayServerName}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Теги */}
|
||||
<div className="d-flex align-items-center gap-2 me-3">
|
||||
<span className={`badge bg-${interfaceTypeColor}-lt text-${interfaceTypeColor}`}>
|
||||
{iface.type}
|
||||
</span>
|
||||
<div className="flex-fill">
|
||||
<div className="fw-medium">{interfaceName}</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} />
|
||||
</div>
|
||||
|
||||
{/* Зеленый блок с IP/доменом */}
|
||||
{displayIp && displayIp !== '—' && (
|
||||
<div className="bg-success-lt rounded px-3 py-2 me-3 d-flex align-items-center">
|
||||
<span className="text-success me-1">→</span>
|
||||
<code className="text-success fw-medium">{displayIp}</code>
|
||||
<button
|
||||
className="btn btn-ghost-success btn-icon btn-sm ms-2"
|
||||
onClick={() => copyToClipboard(displayIp)}
|
||||
title="Копировать"
|
||||
style={{ padding: '2px', width: '20px', height: '20px' }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</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 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">{displayLocalIp}</code>
|
||||
{displayLocalIp && displayLocalIp !== '—' && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(displayLocalIp)}
|
||||
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">{displayRemoteIp}</code>
|
||||
{displayRemoteIp && displayRemoteIp !== '—' && (
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon p-0"
|
||||
onClick={() => copyToClipboard(displayRemoteIp)}
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<IconCopy size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
{iface.name && <span className="text-muted ms-1">({iface.name})</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>
|
||||
{(iface.name2 || iface.name) && (
|
||||
<span className="text-muted ms-1">({iface.name2 || iface.name})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Кнопки действий */}
|
||||
<div className="ms-auto d-flex align-items-center gap-2">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => handleEditInterface(iface)}
|
||||
>
|
||||
<IconEdit size={16} className="me-1" />
|
||||
Изменить
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => handleDeleteInterface(iface)}
|
||||
>
|
||||
<IconTrash size={16} className="me-1" />
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user