refactor(NetworkConfigManager): improve layout and styling of server cards for better user experience and readability
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s

This commit is contained in:
2026-01-25 14:08:30 +07:00
parent 3b858441a2
commit 4feb84338a
+105 -112
View File
@@ -2242,123 +2242,116 @@ function NetworkConfigManager() {
const isUnassigned = serverId === '__unassigned__'; const isUnassigned = serverId === '__unassigned__';
return ( return (
<div key={serverId} className="mb-4"> <div key={serverId} className="card mb-3">
{/* Server header - улучшенная структура с четкой иерархией */} {/* Server header - улучшенная структура с четкой иерархией */}
<div <div
className="card mb-3" className="card-header py-3 px-4"
style={{ style={{
borderLeft: '4px solid rgba(32, 107, 196, 0.3)', backgroundColor: 'rgba(32, 107, 196, 0.06)',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)' borderBottom: '1px solid rgba(32, 107, 196, 0.15)',
borderLeft: '4px solid rgba(32, 107, 196, 0.3)'
}} }}
> >
<div <div className="d-flex align-items-start flex-wrap gap-3">
className="card-header py-3 px-4" {/* Левая часть: основная информация */}
style={{ <div className="d-flex align-items-start flex-grow-1" style={{ minWidth: 0 }}>
backgroundColor: 'rgba(32, 107, 196, 0.06)', {isUnassigned ? (
borderBottom: '1px solid rgba(32, 107, 196, 0.15)' <>
}} <span className="avatar avatar-lg bg-secondary-lt me-3 flex-shrink-0">
> <IconServer size={24} className="text-muted" />
<div className="d-flex align-items-start justify-content-between"> </span>
{/* Левая часть: основная информация */} <div className="flex-grow-1">
<div className="d-flex align-items-start flex-grow-1" style={{ minWidth: 0 }}> <h3 className="mb-0 fw-bold text-muted">Без привязки к серверу</h3>
{isUnassigned ? ( </div>
<> </>
<span className="avatar avatar-lg bg-secondary-lt me-3 flex-shrink-0"> ) : (
<IconServer size={24} className="text-muted" /> <>
</span> <span className="avatar avatar-lg bg-primary-lt me-3 flex-shrink-0">
<div className="flex-grow-1"> <IconServer size={24} className="text-primary" />
<h3 className="mb-1 fw-bold text-muted">Без привязки к серверу</h3> </span>
<div className="flex-grow-1" style={{ minWidth: 0 }}>
{/* Основная строка: название сервера */}
<div className="d-flex align-items-center mb-2 flex-wrap gap-2">
<h3 className="mb-0 fw-bold" style={{ fontSize: '1.25rem', lineHeight: '1.3' }}>
{server?.dns ? server.dns.split('.')[0] : (server?.name || serverId)}
</h3>
{server?.country && (
<span className="badge bg-blue-lt text-blue d-flex align-items-center flex-shrink-0" style={{ fontSize: '0.875rem', padding: '4px 10px' }}>
<span className="me-1" style={{ fontSize: '1rem' }}>{countryToFlag(server.country)}</span>
{server.country}
</span>
)}
{server?.provider && (
<span className={`badge bg-${getProviderColor(server.provider)}-lt text-${getProviderColor(server.provider)} flex-shrink-0`} style={{ fontSize: '0.875rem', padding: '4px 10px' }}>
{server.provider}
</span>
)}
</div> </div>
</>
) : ( {/* Дополнительная информация: DNS и IP */}
<> <div className="d-flex align-items-center flex-wrap gap-3" style={{ fontSize: '0.875rem' }}>
<span className="avatar avatar-lg bg-primary-lt me-3 flex-shrink-0"> {server?.dns && (
<IconServer size={24} className="text-primary" /> <div className="d-flex align-items-center flex-shrink-0">
</span> <span className="text-muted me-1">DNS:</span>
<div className="flex-grow-1" style={{ minWidth: 0 }}> <code className="text-dark" style={{ fontSize: '0.875rem', backgroundColor: 'rgba(0, 0, 0, 0.04)', padding: '2px 6px', borderRadius: '4px', wordBreak: 'break-all' }}>
{/* Основная строка: название сервера */} {server.dns}
<div className="d-flex align-items-center mb-2 flex-wrap gap-2"> </code>
<h3 className="mb-0 fw-bold" style={{ fontSize: '1.25rem' }}> </div>
{server?.dns ? server.dns.split('.')[0] : (server?.name || serverId)} )}
</h3> {server?.ip && (
{server?.country && ( <div className="d-flex align-items-center flex-shrink-0">
<span className="badge bg-blue-lt text-blue d-flex align-items-center" style={{ fontSize: '0.875rem', padding: '4px 10px' }}> <span className="text-muted me-1">IP:</span>
<span className="me-1" style={{ fontSize: '1rem' }}>{countryToFlag(server.country)}</span> <code className="text-dark" style={{ fontSize: '0.875rem', backgroundColor: 'rgba(0, 0, 0, 0.04)', padding: '2px 6px', borderRadius: '4px' }}>
{server.country} {server.ip}
</span> </code>
)} </div>
{server?.provider && ( )}
<span className={`badge bg-${getProviderColor(server.provider)}-lt text-${getProviderColor(server.provider)}`} style={{ fontSize: '0.875rem', padding: '4px 10px' }}>
{server.provider}
</span>
)}
</div>
{/* Дополнительная информация: DNS и IP */}
<div className="d-flex align-items-center flex-wrap gap-3" style={{ fontSize: '0.875rem' }}>
{server?.dns && (
<div className="d-flex align-items-center">
<span className="text-muted me-1">DNS:</span>
<code className="text-dark" style={{ fontSize: '0.875rem', backgroundColor: 'rgba(0, 0, 0, 0.04)', padding: '2px 6px', borderRadius: '4px' }}>
{server.dns}
</code>
</div>
)}
{server?.ip && (
<div className="d-flex align-items-center">
<span className="text-muted me-1">IP:</span>
<code className="text-dark" style={{ fontSize: '0.875rem', backgroundColor: 'rgba(0, 0, 0, 0.04)', padding: '2px 6px', borderRadius: '4px' }}>
{server.ip}
</code>
</div>
)}
</div>
</div> </div>
</> </div>
)} </>
</div> )}
</div>
{/* Правая часть: счетчик и действия */}
<div className="d-flex align-items-center gap-2 flex-shrink-0 ms-3"> {/* Правая часть: счетчик и действия */}
<span className="badge bg-primary text-white" style={{ fontSize: '0.875rem', padding: '6px 12px', fontWeight: '600' }}> <div className="d-flex align-items-center gap-2 flex-shrink-0">
{items.length} {type === 'gateway' ? (items.length === 1 ? 'gateway' : 'gateways') : (items.length === 1 ? 'интерфейс' : 'интерфейсов')} <span className="badge bg-primary text-white" style={{ fontSize: '0.875rem', padding: '6px 12px', fontWeight: '600', whiteSpace: 'nowrap' }}>
</span> {items.length} {type === 'gateway' ? (items.length === 1 ? 'gateway' : 'gateways') : (items.length === 1 ? 'интерфейс' : 'интерфейсов')}
{type === 'interface' && !isUnassigned && items.length > 0 && ( </span>
<button {type === 'interface' && !isUnassigned && items.length > 0 && (
className="btn btn-primary btn-sm" <button
onClick={(e) => { className="btn btn-primary btn-sm flex-shrink-0"
e.stopPropagation(); onClick={(e) => {
handleGenerateMikrotikCodeForServer(serverId, type); e.stopPropagation();
}} handleGenerateMikrotikCodeForServer(serverId, type);
title="Получить код MikroTik для всех интерфейсов этого сервера" }}
style={{ whiteSpace: 'nowrap' }} title="Получить код MikroTik для всех интерфейсов этого сервера"
> style={{ whiteSpace: 'nowrap' }}
<IconCode size={16} className="me-1" /> >
Код для MikroTik <IconCode size={16} className="me-1" />
</button> Код для MikroTik
)} </button>
</div> )}
</div> </div>
</div> </div>
</div>
{/* Items - улучшенное расположение */}
<div className="card-body p-4"> {/* Items - улучшенное расположение */}
{items.length === 0 ? ( <div className="card-body p-4">
<div className="text-center text-muted py-3"> {items.length === 0 ? (
<small>Нет элементов для отображения</small> <div className="text-center text-muted py-3">
</div> <small>Нет элементов для отображения</small>
) : ( </div>
<div className="row g-3"> ) : (
{items.map(item => ( <div className="row row-cards g-3">
<div key={item.id} className="col-12 col-lg-6"> {items.map(item => (
{type === 'gateway' ? renderGatewayCard(item) : <div key={item.id} className="col-12 col-lg-6">
type === 'interface' ? renderInterfaceCard(item, serverId) : {type === 'gateway' ? renderGatewayCard(item) :
type === 'pool' ? renderPoolCard(item) : null} type === 'interface' ? renderInterfaceCard(item, serverId) :
</div> type === 'pool' ? renderPoolCard(item) : null}
))} </div>
</div> ))}
)} </div>
</div> )}
</div> </div>
</div> </div>
); );
@@ -2410,7 +2403,7 @@ function NetworkConfigManager() {
const IndicatorIcon = IconDatabase; const IndicatorIcon = IconDatabase;
return ( return (
<div className="card mb-2" style={{ borderRadius: '12px', maxWidth: '100%' }}> <div className="card" style={{ borderRadius: '12px', maxWidth: '100%' }}>
<div className="card-body p-0"> <div className="card-body p-0">
<div className="d-flex align-items-stretch"> <div className="d-flex align-items-stretch">
{/* Левая часть: тип индикатора */} {/* Левая часть: тип индикатора */}
@@ -2536,7 +2529,7 @@ function NetworkConfigManager() {
} }
return ( return (
<div className="card mb-2" style={{ borderRadius: '12px', maxWidth: '100%' }}> <div className="card" style={{ borderRadius: '12px', maxWidth: '100%' }}>
<div className="card-body p-0"> <div className="card-body p-0">
<div className="d-flex align-items-stretch"> <div className="d-flex align-items-stretch">
{/* Левая часть: тип индикатора (как в ServerCard) */} {/* Левая часть: тип индикатора (как в ServerCard) */}
@@ -2662,7 +2655,7 @@ function NetworkConfigManager() {
const displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp; const displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp;
return ( return (
<div className="card mb-2" style={{ borderRadius: '12px', maxWidth: '100%' }}> <div className="card" style={{ borderRadius: '12px', maxWidth: '100%' }}>
<div className="card-body p-0"> <div className="card-body p-0">
<div className="d-flex align-items-stretch"> <div className="d-flex align-items-stretch">
{/* Левая часть: тип интерфейса (как в ServerCard) */} {/* Левая часть: тип интерфейса (как в ServerCard) */}