refactor(ServerManager, ServerCard): remove unused icons and streamline server card layout; enhance button styles and improve responsiveness
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-01-20 16:29:28 +07:00
parent 4b8fcef17a
commit cf3b1a2028
2 changed files with 73 additions and 93 deletions
+6 -24
View File
@@ -19,7 +19,6 @@ import {
IconAlertTriangle,
IconServer,
IconChevronDown,
IconLink,
IconFilter,
IconMapPin,
IconWorld,
@@ -27,11 +26,9 @@ import {
IconLayoutGrid,
IconNetwork,
IconGitBranch,
IconCopy,
IconSquare,
IconSquareCheck,
IconChevronRight,
IconSettings
IconChevronRight
} from '@tabler/icons-react';
import GraphView from './GraphView';
import {
@@ -659,17 +656,6 @@ function ServerManager() {
</div>
{/* Actions */}
<div className="dropdown">
<button className="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<IconSettings size={16} className="me-1" />
Профили
</button>
<div className="dropdown-menu">
{Object.keys(presets).map(key => (
<button key={key} className="dropdown-item" type="button" onClick={() => applyPreset(key)}>{key}</button>
))}
</div>
</div>
<button type="button" className="btn btn-outline-secondary" onClick={exportAllLinks} disabled={servers.length === 0}>
<IconDownload size={16} className="me-1" /> Экспорт
</button>
@@ -795,8 +781,6 @@ function ServerManager() {
onSelect={toggleServerSelection}
onEdit={handleEdit}
onDelete={confirmDelete}
onGenerateLink={handleGenerateLink}
onQuickCopy={quickCopyLink}
/>
))}
</div>
@@ -865,14 +849,12 @@ function ServerManager() {
<td><span className="badge bg-green-lt text-green">{server.tunnel}</span></td>
<td className="text-end">
<div className="btn-list gap-1 mb-0 justify-content-end">
<button className="btn btn-ghost-primary btn-icon btn-sm" title="Скопировать ссылку" onClick={() => quickCopyLink(server)}>
<IconCopy size={16} />
<button className="btn btn-outline-secondary btn-sm" title="Редактировать" onClick={() => handleEdit(server)}>
<IconEdit size={14} className="me-1" />
Изменить
</button>
<button className="btn btn-ghost-secondary btn-icon btn-sm" title="Редактировать" onClick={() => handleEdit(server)}>
<IconEdit size={16} />
</button>
<button className="btn btn-ghost-danger btn-icon btn-sm" title="Удалить" onClick={() => confirmDelete(server)}>
<IconTrash size={16} />
<button className="btn btn-outline-danger btn-icon btn-sm" title="Удалить" onClick={() => confirmDelete(server)}>
<IconTrash size={14} />
</button>
</div>
</td>
+67 -69
View File
@@ -2,8 +2,6 @@ import {
IconServer,
IconEdit,
IconTrash,
IconLink,
IconCopy,
IconChevronDown,
IconChevronUp,
IconCheck,
@@ -40,9 +38,7 @@ export default function ServerCard({
selected,
onSelect,
onEdit,
onDelete,
onGenerateLink,
onQuickCopy
onDelete
}) {
const [expanded, setExpanded] = useState(false);
@@ -55,112 +51,114 @@ export default function ServerCard({
<div
className={`server-card card mb-2 ${selected ? 'border-primary' : ''}`}
style={{
borderRadius: '8px',
borderRadius: '12px',
borderWidth: selected ? '2px' : '1px',
transition: 'all 0.15s ease'
transition: 'all 0.15s ease',
boxShadow: selected ? '0 0 0 3px rgba(32, 107, 196, 0.15)' : 'none'
}}
>
<div className="card-body p-0">
{/* Основная строка */}
<div className="d-flex align-items-center">
{/* Левая часть: статус + выбор + тип */}
<div className="d-flex align-items-stretch">
{/* Левая часть: тип сервера */}
<div
className={`d-flex align-items-center justify-content-center px-3 py-3 ${isExit ? 'bg-red-lt' : 'bg-blue-lt'}`}
className={`d-flex align-items-center justify-content-center px-3 ${isExit ? 'bg-red-lt' : 'bg-blue-lt'}`}
style={{
minWidth: '100px',
borderTopLeftRadius: selected ? '6px' : '7px',
borderBottomLeftRadius: expanded ? '0' : (selected ? '6px' : '7px')
minWidth: '80px',
borderTopLeftRadius: selected ? '10px' : '11px',
borderBottomLeftRadius: expanded ? '0' : (selected ? '10px' : '11px'),
cursor: 'pointer'
}}
onClick={() => onSelect?.(server)}
>
<button
className="btn p-0 border-0 bg-transparent me-2"
onClick={(e) => { e.stopPropagation(); onSelect?.(server); }}
>
{selected ? (
<IconCircleCheck size={18} className={isExit ? 'text-red' : 'text-blue'} />
) : (
<div style={{ width: 16, height: 16, border: `2px solid ${isExit ? '#d63939' : '#206bc4'}`, borderRadius: '50%', opacity: 0.5 }} />
)}
</button>
<div className="text-center">
{isExit ? (
<IconArrowsRightLeft size={18} className="text-red" />
) : (
<IconRouter size={18} className="text-blue" />
)}
<div className={`fw-bold small ${isExit ? 'text-red' : 'text-blue'}`} style={{ fontSize: '0.7rem', marginTop: '2px' }}>
<div className="mb-1">
{selected ? (
<IconCircleCheck size={20} className={isExit ? 'text-red' : 'text-blue'} />
) : isExit ? (
<IconArrowsRightLeft size={20} className="text-red" />
) : (
<IconRouter size={20} className="text-blue" />
)}
</div>
<div className={`fw-bold ${isExit ? 'text-red' : 'text-blue'}`} style={{ fontSize: '0.65rem', letterSpacing: '0.5px' }}>
{isExit ? 'EXIT' : 'JUMP'}
</div>
</div>
</div>
{/* Центральная часть: информация о сервере */}
<div className="flex-grow-1 py-2 px-3">
<div className="d-flex align-items-center">
<div className="flex-grow-1 py-3 px-3" style={{ cursor: 'pointer' }} onClick={() => setExpanded(!expanded)}>
<div className="d-flex align-items-center flex-wrap gap-3">
{/* Флаг + Название */}
<div className="d-flex align-items-center me-4" style={{ minWidth: '140px' }}>
<span className="me-2" style={{ fontSize: '1.25rem' }}>{countryToFlag(server.country)}</span>
<div className="d-flex align-items-center" style={{ minWidth: '150px' }}>
<span className="me-2" style={{ fontSize: '1.5rem' }}>{countryToFlag(server.country)}</span>
<div>
<div className="fw-semibold" style={{ fontSize: '0.95rem', lineHeight: 1.2 }}>{serverName}</div>
<div className="text-muted" style={{ fontSize: '0.75rem' }}>{server.country}</div>
<div className="fw-semibold" style={{ fontSize: '1rem', lineHeight: 1.2 }}>{serverName}</div>
<div className="text-muted" style={{ fontSize: '0.75rem' }}>{server.provider}</div>
</div>
</div>
{/* Провайдер */}
<div className="me-4" style={{ minWidth: '100px' }}>
<div className="text-muted" style={{ fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Провайдер</div>
<div className="fw-medium" style={{ fontSize: '0.85rem' }}>{server.provider}</div>
</div>
{/* DNS */}
<div className="me-4 flex-grow-1" style={{ minWidth: 0 }}>
<div className="text-muted" style={{ fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Домен</div>
<div style={{ fontSize: '0.85rem', fontFamily: 'ui-monospace, monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<div className="flex-grow-1" style={{ minWidth: '200px' }}>
<div style={{
fontSize: '0.85rem',
fontFamily: 'ui-monospace, SFMono-Regular, monospace',
color: '#666',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{server.dns || server.ip}
</div>
</div>
{/* Туннель + Connections */}
<div className="d-flex align-items-center gap-2 me-3">
<span className="badge bg-secondary-lt text-secondary" style={{ fontSize: '0.7rem' }}>
<IconNetwork size={12} className="me-1" />
{/* Badges: туннель + кол-во шлюзов */}
<div className="d-flex align-items-center gap-2">
<span className="badge bg-secondary-lt text-secondary" style={{ fontSize: '0.75rem', padding: '0.35em 0.65em' }}>
<IconNetwork size={13} className="me-1" style={{ marginTop: '-1px' }} />
{server.tunnel}
</span>
{connectionsCount > 0 && (
<span className="badge bg-green-lt text-green" style={{ fontSize: '0.7rem' }}>
<IconCloud size={12} className="me-1" />
{connectionsCount}
<span className="badge bg-green-lt text-green" style={{ fontSize: '0.75rem', padding: '0.35em 0.65em' }}>
<IconCloud size={13} className="me-1" style={{ marginTop: '-1px' }} />
{connectionsCount} {connectionsCount === 1 ? 'шлюз' : 'шлюзов'}
</span>
)}
</div>
{/* Gateway */}
{/* Primary Gateway */}
{primaryGateway && (
<div className="me-3" style={{ minWidth: '100px' }}>
<div className="text-muted" style={{ fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Шлюз</div>
<div className="fw-medium text-success" style={{ fontSize: '0.85rem' }}>{primaryGateway}</div>
<div className="text-success fw-medium" style={{ fontSize: '0.85rem' }}>
{primaryGateway}
</div>
)}
</div>
</div>
{/* Правая часть: действия */}
<div className="d-flex align-items-center gap-1 px-2 border-start" style={{ height: '60px' }}>
<button className="btn btn-ghost-primary btn-icon btn-sm" title="Скопировать" onClick={(e) => { e.stopPropagation(); onQuickCopy?.(server); }}>
<IconCopy size={16} />
<div className="d-flex align-items-center gap-2 px-3 border-start">
<button
className="btn btn-outline-secondary btn-sm d-flex align-items-center gap-1"
title="Редактировать"
onClick={(e) => { e.stopPropagation(); onEdit?.(server); }}
>
<IconEdit size={15} />
<span className="d-none d-md-inline">Изменить</span>
</button>
<button className="btn btn-ghost-secondary btn-icon btn-sm" title="Ссылка" onClick={(e) => { e.stopPropagation(); onGenerateLink?.(server); }}>
<IconLink size={16} />
<button
className="btn btn-outline-danger btn-sm d-flex align-items-center gap-1"
title="Удалить"
onClick={(e) => { e.stopPropagation(); onDelete?.(server); }}
>
<IconTrash size={15} />
</button>
<button className="btn btn-ghost-secondary btn-icon btn-sm" title="Редактировать" onClick={(e) => { e.stopPropagation(); onEdit?.(server); }}>
<IconEdit size={16} />
</button>
<button className="btn btn-ghost-danger btn-icon btn-sm" title="Удалить" onClick={(e) => { e.stopPropagation(); onDelete?.(server); }}>
<IconTrash size={16} />
</button>
<div className="vr mx-1" style={{ height: '24px' }}></div>
<button className="btn btn-ghost-secondary btn-icon btn-sm" onClick={() => setExpanded(!expanded)} title={expanded ? 'Свернуть' : 'Развернуть'}>
{expanded ? <IconChevronUp size={16} /> : <IconChevronDown size={16} />}
<button
className="btn btn-ghost-secondary btn-icon btn-sm ms-1"
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
title={expanded ? 'Свернуть' : 'Подробнее'}
>
{expanded ? <IconChevronUp size={18} /> : <IconChevronDown size={18} />}
</button>
</div>
</div>