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