refactor(Managers): apply flat card styling across ASNs, Domains, and IPRanges managers for enhanced visual consistency and improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m1s

This commit is contained in:
2026-02-16 17:28:51 +07:00
parent eca744999c
commit dbc94d71a0
3 changed files with 440 additions and 437 deletions
+127 -126
View File
@@ -845,7 +845,7 @@ function IPRangesManager() {
onExport={selectedItems.size > 0 ? handleBulkExport : null}
/>
<div className="card">
<div className="card card-flat">
<div className="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h3 className="card-title mb-0">Список IP-диапазонов <span className="badge bg-blue-lt text-blue ms-2">{items.length}</span></h3>
<div className="d-flex gap-2 flex-wrap align-items-center">
@@ -918,133 +918,134 @@ function IPRangesManager() {
</div>
</div>
</div>
</div>
{/* Таблица без карточной подложки */}
{loading ? (
<TableSkeleton rows={10} cols={3} flat />
) : paginatedItems.length === 0 ? (
<TableEmpty cols={3} flat>
<EmptyState
title="Нет IP-диапазонов"
description="Импортируйте или добавьте записи, чтобы начать."
action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>}
secondaryAction={<button className="btn btn-outline-primary" onClick={downloadSampleCsv}>Шаблон CSV</button>}
/>
</TableEmpty>
) : (
<div className="table-responsive">
<table className="table table-flat table-vcenter table-nowrap mb-0">
<thead>
<tr>
<th style={{width: '40px'}}>
<input
type="checkbox"
className="form-check-input"
checked={paginatedItems.length > 0 && paginatedItems.every(i => selectedItems.has(i.ipRange))}
onChange={(e) => {
if (e.target.checked) {
selectAll();
} else {
deselectAll();
}
}}
title="Выбрать все на странице"
/>
</th>
<th className="cursor-pointer user-select-none" onClick={() => handleSort('ipRange')}>
<div className="d-flex align-items-center">
<IconNetwork size={16} className="me-1 text-muted" />
IP-диапазон
{sortField === 'ipRange' && (
<span className="ms-1">
{sortOrder === 'asc' ? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}
</span>
)}
</div>
</th>
<th className="cursor-pointer user-select-none" onClick={() => handleSort('community')}>
<div className="d-flex align-items-center">
<IconHash size={16} className="me-1 text-muted" />
Community
{sortField === 'community' && (
<span className="ms-1">
{sortOrder === 'asc' ? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}
</span>
)}
</div>
</th>
<th className="text-end">Действия</th>
</tr>
</thead>
<tbody>
{paginatedItems.map((item, idx) => (
<tr key={item.ipRange} className={editingIp === item.ipRange ? 'table-active' : (selectedItems.has(item.ipRange) ? 'table-selected' : '')} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}>
<td>
<input
type="checkbox"
className="form-check-input"
checked={selectedItems.has(item.ipRange)}
onChange={() => toggleSelectItem(item.ipRange)}
onClick={(e) => e.stopPropagation()}
/>
</td>
<td className="font-monospace" title="CIDR">
<div className="d-flex align-items-center">
<IconNetwork size={16} className="me-2 text-muted" />
<span>{item.ipRange}</span>
<Tooltip content="Копировать IP-диапазон" position="top">
<button
className="btn btn-ghost-secondary btn-icon btn-sm ms-2"
onClick={() => copyToClipboard(item.ipRange)}
aria-label="Копировать"
title="Копировать IP-диапазон"
>
<IconCopy size={14} />
</button>
</Tooltip>
</div>
</td>
<td>{renderCommunityBadge(item.community)}</td>
<td className="text-end">
{editingIp === item.ipRange ? (
<>
<CommunityAutocompleteInput
value={editingValue}
onChange={setEditingValue}
communities={communities}
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
style={{ width: '150px' }}
onKeyDown={(e) => handleEditKeyDown(e, item.ipRange)}
useShortValue={true}
<div className="card-body p-0">
{/* Таблица на лёгкой карточной подложке */}
{loading ? (
<TableSkeleton rows={10} cols={3} flat />
) : paginatedItems.length === 0 ? (
<TableEmpty cols={3} flat>
<EmptyState
title="Нет IP-диапазонов"
description="Импортируйте или добавьте записи, чтобы начать."
action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>}
secondaryAction={<button className="btn btn-outline-primary" onClick={downloadSampleCsv}>Шаблон CSV</button>}
/>
</TableEmpty>
) : (
<div className="table-responsive">
<table className="table table-flat table-vcenter table-nowrap mb-0">
<thead>
<tr>
<th style={{width: '40px'}}>
<input
type="checkbox"
className="form-check-input"
checked={paginatedItems.length > 0 && paginatedItems.every(i => selectedItems.has(i.ipRange))}
onChange={(e) => {
if (e.target.checked) {
selectAll();
} else {
deselectAll();
}
}}
title="Выбрать все на странице"
/>
</th>
<th className="cursor-pointer user-select-none" onClick={() => handleSort('ipRange')}>
<div className="d-flex align-items-center">
<IconNetwork size={16} className="me-1 text-muted" />
IP-диапазон
{sortField === 'ipRange' && (
<span className="ms-1">
{sortOrder === 'asc' ? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}
</span>
)}
</div>
</th>
<th className="cursor-pointer user-select-none" onClick={() => handleSort('community')}>
<div className="d-flex align-items-center">
<IconHash size={16} className="me-1 text-muted" />
Community
{sortField === 'community' && (
<span className="ms-1">
{sortOrder === 'asc' ? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}
</span>
)}
</div>
</th>
<th className="text-end">Действия</th>
</tr>
</thead>
<tbody>
{paginatedItems.map((item, idx) => (
<tr key={item.ipRange} className={editingIp === item.ipRange ? 'table-active' : (selectedItems.has(item.ipRange) ? 'table-selected' : '')} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}>
<td>
<input
type="checkbox"
className="form-check-input"
checked={selectedItems.has(item.ipRange)}
onChange={() => toggleSelectItem(item.ipRange)}
onClick={(e) => e.stopPropagation()}
/>
<Tooltip content="Сохранить (Enter)">
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.ipRange)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
</Tooltip>
<Tooltip content="Отмена (Esc)">
<button className="btn btn-secondary btn-icon btn-sm" onClick={handleCancelEdit}><IconX size={16} /></button>
</Tooltip>
</>
) : (
<>
<Tooltip content="Редактировать" position="top">
<button className="btn btn-outline-primary btn-icon btn-sm me-1" aria-label="Редактировать" onClick={() => handleEdit(item)} title="Редактировать"><IconEdit size={16} /></button>
</Tooltip>
<Tooltip content="Копировать" position="top">
<button className="btn btn-outline-secondary btn-icon btn-sm me-1" aria-label="Копировать" onClick={() => copyToClipboard(`${item.ipRange} ${item.community}`)} title="Копировать"><IconCopy size={16} /></button>
</Tooltip>
<Tooltip content="Удалить" position="top">
<button className="btn btn-outline-danger btn-icon btn-sm" aria-label="Удалить" onClick={() => confirmDelete(item)} title="Удалить"><IconTrash size={16} /></button>
</Tooltip>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</td>
<td className="font-monospace" title="CIDR">
<div className="d-flex align-items-center">
<IconNetwork size={16} className="me-2 text-muted" />
<span>{item.ipRange}</span>
<Tooltip content="Копировать IP-диапазон" position="top">
<button
className="btn btn-ghost-secondary btn-icon btn-sm ms-2"
onClick={() => copyToClipboard(item.ipRange)}
aria-label="Копировать"
title="Копировать IP-диапазон"
>
<IconCopy size={14} />
</button>
</Tooltip>
</div>
</td>
<td>{renderCommunityBadge(item.community)}</td>
<td className="text-end">
{editingIp === item.ipRange ? (
<>
<CommunityAutocompleteInput
value={editingValue}
onChange={setEditingValue}
communities={communities}
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
style={{ width: '150px' }}
onKeyDown={(e) => handleEditKeyDown(e, item.ipRange)}
useShortValue={true}
/>
<Tooltip content="Сохранить (Enter)">
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.ipRange)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
</Tooltip>
<Tooltip content="Отмена (Esc)">
<button className="btn btn-secondary btn-icon btn-sm" onClick={handleCancelEdit}><IconX size={16} /></button>
</Tooltip>
</>
) : (
<>
<Tooltip content="Редактировать" position="top">
<button className="btn btn-outline-primary btn-icon btn-sm me-1" aria-label="Редактировать" onClick={() => handleEdit(item)} title="Редактировать"><IconEdit size={16} /></button>
</Tooltip>
<Tooltip content="Копировать" position="top">
<button className="btn btn-outline-secondary btn-icon btn-sm me-1" aria-label="Копировать" onClick={() => copyToClipboard(`${item.ipRange} ${item.community}`)} title="Копировать"><IconCopy size={16} /></button>
</Tooltip>
<Tooltip content="Удалить" position="top">
<button className="btn btn-outline-danger btn-icon btn-sm" aria-label="Удалить" onClick={() => confirmDelete(item)} title="Удалить"><IconTrash size={16} /></button>
</Tooltip>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
{/* Пагинация */}
<Pagination
currentPage={currentPage}