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