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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user