refactor(NetworkConfigManager): improve layout and styling of gateway and interface cards; enhance indicator display and text formatting for better clarity
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-01-22 14:43:43 +07:00
parent 00bf38fbb9
commit b79dced147
+142 -95
View File
@@ -24,7 +24,8 @@ import {
IconCircleFilled,
IconLayoutGrid,
IconList,
IconCode
IconCode,
IconArrowsRightLeft
} from '@tabler/icons-react';
/**
@@ -922,72 +923,99 @@ function NetworkConfigManager() {
? getParentGateway(gateway.parentGatewayId)
: null;
// Определяем тип индикатора
// Определяем тип индикатора (как в ServerCard)
const indicatorType = gateway.type === 'recursive' ? 'EXIT' : 'JUMP';
const indicatorColor = gateway.type === 'recursive' ? 'red' : 'blue';
const indicatorIcon = gateway.type === 'recursive' ? IconRouter : IconWorld;
const IndicatorIcon = gateway.type === 'recursive' ? IconArrowsRightLeft : IconRouter;
// Определяем отображаемый IP/домен для зеленого блока
const displayIp = parentGateway
? (parentGateway.parentType === 'interface'
? (parentGateway.name || parentGateway.interfaceType || 'interface')
: parentGateway.ip)
: gateway.ip;
// Формируем текст для зеленого блока
let displayText = '';
if (gateway.type === 'direct') {
// Для прямого gateway: 0.0.0.0/0 -> IP адрес
displayText = gateway.ip ? `0.0.0.0/0 → ${gateway.ip}` : '';
} else if (gateway.type === 'recursive' && parentGateway) {
// Для рекурсивного: IP адрес -> IP адрес родительского gateway
const parentIp = parentGateway.parentType === 'interface'
? parentGateway.remoteIp
: parentGateway.ip;
displayText = gateway.ip && parentIp ? `${gateway.ip}${parentIp}` : '';
}
return (
<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 key={gateway.id} className="card mb-2" style={{ borderRadius: '12px', maxWidth: '100%' }}>
<div className="card-body p-0">
<div className="d-flex align-items-stretch">
{/* Левая часть: тип индикатора (как в ServerCard) */}
<div
className={`d-flex align-items-center justify-content-center px-3 bg-${indicatorColor}-lt`}
style={{
minWidth: '80px',
borderTopLeftRadius: '11px',
borderBottomLeftRadius: '11px'
}}
>
<div className="text-center">
<div className="mb-1">
<IndicatorIcon size={20} className={`text-${indicatorColor}`} />
</div>
<div className={`fw-bold text-${indicatorColor}`} style={{ fontSize: '0.65rem', letterSpacing: '0.5px' }}>
{indicatorType}
</div>
</div>
<span className={`text-${indicatorColor} fw-bold`} style={{ fontSize: '0.7rem' }}>{indicatorType}</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">{gateway.description || provider || gateway.ip || '—'}</div>
{/* Центральная часть: информация */}
<div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0 }}>
{/* Верхняя строка: флаг, название, провайдер */}
<div className="d-flex align-items-center mb-1">
<div className="d-flex align-items-center me-3" style={{ minWidth: 0 }}>
{displayCountry && (
<span className="me-2" style={{ fontSize: '1.25rem' }}>{countryToFlag(displayCountry)}</span>
)}
<div style={{ minWidth: 0 }}>
<span className="fw-semibold">{gateway.description || provider || gateway.ip || '—'}</span>
{provider && (
<>
<span className="text-muted mx-2"></span>
<span className="text-muted">{provider}</span>
</>
)}
</div>
</div>
{/* Зеленый блок с маршрутом */}
{displayText && (
<span className="badge bg-success-lt text-success ms-auto" style={{ whiteSpace: 'nowrap' }}>
{displayText}
</span>
)}
</div>
{/* Нижняя строка: DNS + badges */}
<div className="d-flex align-items-center gap-2">
{server && (
<div className="text-muted small">{server.dns || server.ip || gateway.serverId}</div>
<code className="text-muted" style={{ fontSize: '0.8rem' }}>
{server.dns || server.ip || gateway.serverId}
</code>
)}
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`}>
{gatewayType.label}
</span>
{gateway.ip && (
<button
className="btn btn-ghost-secondary btn-icon btn-sm p-1"
onClick={() => copyToClipboard(gateway.ip)}
title="Копировать IP"
style={{ width: '20px', height: '20px' }}
>
<IconCopy size={12} />
</button>
)}
</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">
{/* Правая часть: действия */}
<div className="d-flex align-items-center gap-2 px-3 border-start">
<button
className="btn btn-outline-secondary btn-sm"
onClick={() => handleEditGateway(gateway)}
@@ -1049,55 +1077,74 @@ function NetworkConfigManager() {
const displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp;
return (
<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 key={iface.id} className="card mb-2" style={{ borderRadius: '12px', maxWidth: '100%' }}>
<div className="card-body p-0">
<div className="d-flex align-items-stretch">
{/* Левая часть: тип интерфейса (как в ServerCard) */}
<div
className={`d-flex align-items-center justify-content-center px-3 bg-${interfaceTypeColor}-lt`}
style={{
minWidth: '80px',
borderTopLeftRadius: '11px',
borderBottomLeftRadius: '11px'
}}
>
<div className="text-center">
<div className="mb-1">
<IconRouter size={20} className={`text-${interfaceTypeColor}`} />
</div>
<div className={`fw-bold text-${interfaceTypeColor}`} style={{ fontSize: '0.65rem', letterSpacing: '0.5px' }}>
{iface.type}
</div>
</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>
{/* Центральная часть: информация */}
<div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0 }}>
{/* Верхняя строка: флаг, название */}
<div className="d-flex align-items-center mb-1">
<div className="d-flex align-items-center me-3" style={{ minWidth: 0 }}>
{displayCountry && (
<span className="me-2" style={{ fontSize: '1.25rem' }}>{countryToFlag(displayCountry)}</span>
)}
<div style={{ minWidth: 0 }}>
<span className="fw-semibold">{interfaceName}</span>
</div>
</div>
{/* Зеленый блок с IP */}
{displayIp && displayIp !== '—' && (
<span className="badge bg-success-lt text-success ms-auto" style={{ whiteSpace: 'nowrap' }}>
{displayIp}
</span>
)}
</div>
{/* Нижняя строка: DNS + badges */}
<div className="d-flex align-items-center gap-2">
{displayServerName && (
<div className="text-muted small">{displayServerName}</div>
<code className="text-muted" style={{ fontSize: '0.8rem' }}>
{displayServerName}
</code>
)}
<span className={`badge bg-${interfaceTypeColor}-lt text-${interfaceTypeColor}`}>
{iface.type}
</span>
{displayIp && displayIp !== '—' && (
<button
className="btn btn-ghost-secondary btn-icon btn-sm p-1"
onClick={() => copyToClipboard(displayIp)}
title="Копировать IP"
style={{ width: '20px', height: '20px' }}
>
<IconCopy size={12} />
</button>
)}
</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>
{/* Зеленый блок с 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>
)}
{/* Кнопки действий */}
<div className="ms-auto d-flex align-items-center gap-2">
{/* Правая часть: действия */}
<div className="d-flex align-items-center gap-2 px-3 border-start">
<button
className="btn btn-outline-secondary btn-sm"
onClick={() => handleEditInterface(iface)}