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,
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user