From b34bf4af953c5293ec0ecfe20affdf4bb3798c23 Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 11 Nov 2025 19:39:28 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D1=87=D0=B5=D0=BA=20=D0=B8=20=D1=83=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=85=20ASNsNewManager,=20DomainsNewManager?= =?UTF-8?q?=20=D0=B8=20IPRangesManager.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/ASNsNewManager.jsx | 346 +++++++++++++------ frontend/src/DomainsNewManager.jsx | 535 +++++++++++++++++++++-------- frontend/src/IPRangesManager.jsx | 313 +++++++++++------ frontend/src/index.css | 136 ++++++++ 4 files changed, 966 insertions(+), 364 deletions(-) diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index b16fd05..6c96b65 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -16,7 +16,10 @@ import { IconDeviceFloppy, IconHash, IconClock, - IconFileText + IconFileText, + IconFilter, + IconArrowUp, + IconArrowDown } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; @@ -33,6 +36,7 @@ import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; import ImportModal from './components/ImportModal.jsx'; import QuickAddBar from './components/QuickAddBar.jsx'; import AccordionCard from './components/AccordionCard.jsx'; +import SavedFilters from './components/SavedFilters.jsx'; import { getAsnName, getAsnNameSync } from './lib/asn.js'; const API_URL = '/api'; @@ -487,12 +491,72 @@ function ASNsNewManager() { /> )} /> + {/* Статистические карточки */} +
+
+
+
+
+
+ + + +
+
+
Всего ASN
+
+ {items.length} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Отфильтровано
+
+ {filtered.length} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Уникальных Community
+
+ {allCommunities.length} +
+
+
+
+
+
+
- {/* Classic Add New ASN Card (restored) */} -
-
-

+ {/* Classic Add New ASN Card (improved) */} +
+
+

Добавить новый ASN

@@ -500,46 +564,64 @@ function ASNsNewManager() {
{ e.preventDefault(); handleAddItem(); }}>
- - setNewItem({ ...newItem, asn: e.target.value })} - /> + +
+ + + + setNewItem({ ...newItem, asn: e.target.value })} + /> +
{newInvalid.asn &&
Только цифры
} -
Формат: только цифры, например 12345
+ {isValidAsn(newItem.asn) && !newInvalid.asn && (
Корректный номер AS
)} +
Формат: только цифры, например 12345
- - setNewItem({ ...newItem, community: v })} - communities={communities} - placeholder="Начните вводить номер или имя" - className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} - /> - {newInvalid.community &&
Только цифры
} + +
+ + + + setNewItem({ ...newItem, community: v })} + communities={communities} + placeholder="Начните вводить номер или имя" + className={`form-control${newInvalid.community ? ' is-invalid' : isValidCommunity(newItem.community) ? ' is-valid' : ''}`} + /> +
+ {newInvalid.community &&
Неверный формат community
} + {isValidCommunity(newItem.community) && !newInvalid.community && (
Корректный формат community
)}
Формат: N или N:N (например 65000:100)
{(() => { const match = communities.find(c => c.value === String(newItem.community).trim()); if (!match) return null; return ( -
- {match.name && (<>{match.name} — )} - {match.description || ''} - {match.tags && match.tags.length > 0 && ( - <> ({match.tags.join(', ')}) - )} +
+
+
+ {match.name || match.value} + {match.description &&
{match.description}
} + {match.tags && match.tags.length > 0 && ( +
+ {match.tags.map((tag, idx) => ({tag}))} +
+ )} +
+
); })()}
-
@@ -547,7 +629,7 @@ function ASNsNewManager() {
{/* Дополнительные варианты */} - + -
{ e.preventDefault(); }} - onDrop={onDropImport} +
{ e.preventDefault(); e.currentTarget.style.borderColor = 'var(--tblr-primary)'; }} + onDragLeave={(e) => { e.currentTarget.style.borderColor = ''; }} + onDrop={(e) => { e.currentTarget.style.borderColor = ''; onDropImport(e); }} > -
- Перетащите файл TXT/CSV сюда для импорта (формат: ASN community) +
+ +
Перетащите файл TXT/CSV сюда
+
Формат: ASN community
- {/* Drag & Drop импорт */} -
{ e.preventDefault(); }} - onDrop={onDropImport} - > -
- Перетащите файл TXT/CSV сюда для импорта (формат: ASN community) -
-
+ {/* Карточка действий больше не нужна — действия перенесены в page-header */}
@@ -592,10 +669,26 @@ function ASNsNewManager() {

Список ASN {items.length}

-
+
+ { + if (filters.searchTerm !== undefined) setSearchTerm(filters.searchTerm); + if (filters.filterCommunity !== undefined) setFilterCommunity(filters.filterCommunity); + if (filters.sortField !== undefined) setSortField(filters.sortField); + if (filters.sortOrder !== undefined) setSortOrder(filters.sortOrder); + setCurrentPage(1); + }} + /> {/* Фильтр по community */} - { setFilterCommunity(e.target.value); setCurrentPage(1); }} title={(() => { @@ -616,9 +709,10 @@ function ASNsNewManager() { ); })} - + +
{/* Поиск */} -
+
@@ -629,7 +723,19 @@ function ASNsNewManager() { value={searchTerm} onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }} aria-label="Поиск ASN" + style={{ paddingRight: searchTerm ? '2.5rem' : undefined }} /> + {!!searchTerm && ( + + )}
@@ -657,32 +763,43 @@ function ASNsNewManager() { - - - {paginatedItems.map((item) => ( - - + {paginatedItems.map((item, idx) => ( + + - - {paginatedItems.map((item) => ( - + {paginatedItems.map((item, idx) => ( + - - + + ))} @@ -802,43 +1000,84 @@ function DomainsNewManager() { )} {/* Пагинация */} {totalPages > 1 && ( -
-
- Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || paginatedItems.length} +
+
+
+ Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || filtered.length} +
+
    +
  • + +
  • +
  • + +
  • + {(() => { + const pages = []; + let start = Math.max(1, currentPage - 2); + let end = Math.min(totalPages, currentPage + 2); + if (currentPage <= 3) end = Math.min(totalPages, 5); + if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4); + if (start > 1) pages.push('start-ellipsis'); + for (let p = start; p <= end; p++) pages.push(p); + if (end < totalPages) pages.push('end-ellipsis'); + return pages.map((p) => ( + p === 'start-ellipsis' || p === 'end-ellipsis' ? ( +
  • + +
  • + ) : ( +
  • + +
  • + ) + )); + })()} +
  • + +
  • +
  • + +
  • +
-
    -
  • - -
  • -
  • - -
  • - {(() => { - const pages = []; - let start = Math.max(1, currentPage - 2); - let end = Math.min(totalPages, currentPage + 2); - if (currentPage <= 3) end = Math.min(totalPages, 5); - if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4); - if (start > 1) pages.push('start-ellipsis'); - for (let p = start; p <= end; p++) pages.push(p); - if (end < totalPages) pages.push('end-ellipsis'); - return pages.map((p) => ( - p === 'start-ellipsis' || p === 'end-ellipsis' ? ( -
  • - ) : ( -
  • - -
  • - ) - )); - })()} -
  • - -
  • -
  • - -
  • -
)} diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 53800a3..354b78e 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -16,7 +16,11 @@ import { IconDeviceFloppy, IconHash, IconClock, - IconFileText + IconFileText, + IconFilter, + IconArrowUp, + IconArrowDown, + IconNetwork } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; @@ -33,6 +37,7 @@ import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; import ImportModal from './components/ImportModal.jsx'; import QuickAddBar from './components/QuickAddBar.jsx'; import AccordionCard from './components/AccordionCard.jsx'; +import SavedFilters from './components/SavedFilters.jsx'; const API_URL = '/api'; @@ -740,12 +745,72 @@ function IPRangesManager() { /> )} /> + {/* Статистические карточки */} +
+
+
+
+
+
+ + + +
+
+
Всего диапазонов
+
+ {items.length} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Отфильтровано
+
+ {filtered.length} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Уникальных Community
+
+ {allCommunities.length} +
+
+
+
+
+
+
- {/* Classic Add New Item Card (restored) */} -
-
-

+ {/* Classic Add New Item Card (improved) */} +
+
+

Добавить новый IP-диапазон

@@ -753,45 +818,63 @@ function IPRangesManager() {
{ e.preventDefault(); handleAddItem(); }}>
- - setNewItem({ ...newItem, ipRange: e.target.value })} - /> + +
+ + + + setNewItem({ ...newItem, ipRange: e.target.value })} + /> +
{newInvalid.ipRange &&
Некорректный CIDR
} -
Формат: A.B.C.D/Маска, например 10.0.0.0/24
+ {isValidCidr(newItem.ipRange) && !newInvalid.ipRange && (
Корректный CIDR
)} +
Формат: A.B.C.D/Маска, например 10.0.0.0/24
- - setNewItem({ ...newItem, community: v })} - communities={communities} - placeholder="Начните вводить номер или имя" - className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} - /> - {newInvalid.community &&
Только цифры
} + +
+ + + + setNewItem({ ...newItem, community: v })} + communities={communities} + placeholder="Начните вводить номер или имя" + className={`form-control${newInvalid.community ? ' is-invalid' : isValidCommunity(newItem.community) ? ' is-valid' : ''}`} + /> +
+ {newInvalid.community &&
Неверный формат community
} + {isValidCommunity(newItem.community) && !newInvalid.community && (
Корректный формат community
)} {(() => { const match = communities.find(c => c.value === String(newItem.community).trim()); if (!match) return null; return ( -
- {match.name && (<>{match.name} — )} - {match.description || ''} - {match.tags && match.tags.length > 0 && ( - <> ({match.tags.join(', ')}) - )} +
+
+
+ {match.name || match.value} + {match.description &&
{match.description}
} + {match.tags && match.tags.length > 0 && ( +
+ {match.tags.map((tag, idx) => ({tag}))} +
+ )} +
+
); })()}
-
@@ -799,7 +882,7 @@ function IPRangesManager() {
{/* Дополнительные варианты */} - + -
{ e.preventDefault(); }} - onDrop={onDropImport} +
{ e.preventDefault(); e.currentTarget.style.borderColor = 'var(--tblr-primary)'; }} + onDragLeave={(e) => { e.currentTarget.style.borderColor = ''; }} + onDrop={(e) => { e.currentTarget.style.borderColor = ''; onDropImport(e); }} > -
- Перетащите файл TXT/CSV сюда для импорта (формат: CIDR community) +
+ +
Перетащите файл TXT/CSV сюда
+
Формат: CIDR community
- {/* Drag & Drop импорт */} -
{ e.preventDefault(); }} - onDrop={onDropImport} - > -
- Перетащите файл TXT/CSV сюда для импорта (формат: CIDR community) -
-
+ {/* Карточка действий больше не нужна — действия перенесены в page-header */}
@@ -908,29 +986,40 @@ function IPRangesManager() {

handleSort('asn')}> - Номер AS - {sortField === 'asn' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} + handleSort('asn')}> +
+ + Номер AS + {sortField === 'asn' && ( + + {sortOrder === 'asc' ? : } + + )} +
Имя AS handleSort('community')}> - Community - {sortField === 'community' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} + handleSort('community')}> +
+ + Community + {sortField === 'community' && ( + + {sortOrder === 'asc' ? : } + + )} +
{item.asn}
+
+ + {item.asn} +
+
{asnNameMap[item.asn] ? ( {asnNameMap[item.asn]} @@ -699,10 +816,11 @@ function ASNsNewManager() { onChange={setEditingValue} communities={communities} onKeyDown={(e) => handleEditKeyDown(e, item.asn)} - className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} + className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} + style={{ width: '150px' }} /> - - + + ) : ( <> @@ -719,43 +837,55 @@ function ASNsNewManager() { )} {/* Пагинация */} {totalPages > 1 && ( -
-
- Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || paginatedItems.length} +
+
+
+ Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || filtered.length} +
+
    +
  • + +
  • +
  • + +
  • + {(() => { + const pages = []; + let start = Math.max(1, currentPage - 2); + let end = Math.min(totalPages, currentPage + 2); + if (currentPage <= 3) end = Math.min(totalPages, 5); + if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4); + if (start > 1) pages.push('start-ellipsis'); + for (let p = start; p <= end; p++) pages.push(p); + if (end < totalPages) pages.push('end-ellipsis'); + return pages.map((p) => ( + p === 'start-ellipsis' || p === 'end-ellipsis' ? ( +
  • + ) : ( +
  • + +
  • + ) + )); + })()} +
  • + +
  • +
  • + +
  • +
-
    -
  • - -
  • -
  • - -
  • - {(() => { - const pages = []; - let start = Math.max(1, currentPage - 2); - let end = Math.min(totalPages, currentPage + 2); - if (currentPage <= 3) end = Math.min(totalPages, 5); - if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4); - if (start > 1) pages.push('start-ellipsis'); - for (let p = start; p <= end; p++) pages.push(p); - if (end < totalPages) pages.push('end-ellipsis'); - return pages.map((p) => ( - p === 'start-ellipsis' || p === 'end-ellipsis' ? ( -
  • - ) : ( -
  • - -
  • - ) - )); - })()} -
  • - -
  • -
  • - -
  • -
)} diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 91465a0..e17fe3f 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -18,7 +18,12 @@ import { IconHash, IconClock, IconFileText, - IconFilter + IconFilter, + IconWorld, + IconTrendingUp, + IconUsers, + IconArrowUp, + IconArrowDown } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; import PageHeader from './components/PageHeader.jsx'; @@ -530,12 +535,103 @@ function DomainsNewManager() { /> )} /> + + {/* Статистические карточки */} +
+
+
+
+
+
+ + + +
+
+
Всего доменов
+
+ {items.length} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Отфильтровано
+
+ {filtered.length} + {filtered.length !== items.length && ( + + ({items.length - filtered.length} скрыто) + + )} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Уникальных Community
+
+ {allCommunities.length} +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
Выбрано
+
+ {selectedDomains.size} + {selectedDomains.size > 0 && ( + + ({Math.round((selectedDomains.size / filtered.length) * 100)}%) + + )} +
+
+
+
+
+
+
+
- {/* Classic Add New Domain Card (restored) */} + {/* Classic Add New Domain Card (improved) */}
-
-

+
+

Добавить новый домен

@@ -543,47 +639,78 @@ function DomainsNewManager() {
{ e.preventDefault(); handleAddItem(); }}>
- - setNewItem({ ...newItem, domain: e.target.value })} - /> + +
+ + + + setNewItem({ ...newItem, domain: e.target.value })} + /> +
{newInvalid.domain &&
Неверный формат домена
} -
Без http/https, только доменное имя
-
Формат: sub.domain.tld (например foo.example.org)
+ {isValidDomain(newItem.domain) && !newInvalid.domain && ( +
Корректный формат домена
+ )} +
+ Формат: sub.domain.tld (например foo.example.org) +
- - setNewItem({ ...newItem, community: v })} - communities={communities} - placeholder="Начните вводить номер или имя" - className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} - /> - {newInvalid.community &&
Только цифры
} -
Формат: N или N:N (например 65000:100)
+ +
+ + + + setNewItem({ ...newItem, community: v })} + communities={communities} + placeholder="Начните вводить номер или имя" + className={`form-control${newInvalid.community ? ' is-invalid' : isValidCommunity(newItem.community) ? ' is-valid' : ''}`} + /> +
+ {newInvalid.community &&
Неверный формат community
} + {isValidCommunity(newItem.community) && !newInvalid.community && ( +
Корректный формат community
+ )} +
+ Формат: N или N:N (например 65000:100) +
{(() => { const match = communities.find(c => c.value === String(newItem.community).trim()); if (!match) return null; return ( -
- {match.name && (<>{match.name} — )} - {match.description || ''} - {match.tags && match.tags.length > 0 && ( - <> ({match.tags.join(', ')}) - )} +
+
+
+ {match.name || match.value} + {match.description &&
{match.description}
} + {match.tags && match.tags.length > 0 && ( +
+ {match.tags.map((tag, idx) => ( + {tag} + ))} +
+ )} +
+
); })()}
-
@@ -609,28 +736,20 @@ function DomainsNewManager() { }); }} /> -
{ e.preventDefault(); }} - onDrop={onDropImport} +
{ e.preventDefault(); e.currentTarget.style.borderColor = 'var(--tblr-primary)'; }} + onDragLeave={(e) => { e.currentTarget.style.borderColor = ''; }} + onDrop={(e) => { e.currentTarget.style.borderColor = ''; onDropImport(e); }} > -
- Перетащите файл TXT/CSV сюда для импорта (формат: domain community) +
+ +
Перетащите файл TXT/CSV сюда
+
Формат: domain community
- - {/* Карточка действий больше не нужна — действия перенесены в page-header */} - {/* Drag & Drop импорт */} -
{ e.preventDefault(); }} - onDrop={onDropImport} - > -
- Перетащите файл TXT/CSV сюда для импорта (формат: domain community) -
-
@@ -646,15 +765,16 @@ function DomainsNewManager() {
-
+

+ Список доменов {filtered.length}

-
+
{/* Сохранённые фильтры */} {/* Фильтр по community */} - +
+ + + + +
{/* Поиск */} -
+
@@ -705,7 +831,25 @@ function DomainsNewManager() { value={searchTerm} onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }} aria-label="Поиск домена" + style={{ paddingRight: searchTerm ? '2.5rem' : undefined }} /> + {searchTerm && ( + + )}
@@ -742,57 +886,111 @@ function DomainsNewManager() { onChange={(e) => e.target.checked ? handleSelectAll() : handleDeselectAll()} /> -

handleSort('domain')}> - Домен - {sortField === 'domain' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} + handleSort('domain')}> +
+ + Домен + {sortField === 'domain' && ( + + {sortOrder === 'asc' ? : } + + )} +
handleSort('community')}> - Community - {sortField === 'community' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} + handleSort('community')}> +
+ + Community + {sortField === 'community' && ( + + {sortOrder === 'asc' ? : } + + )} +
handleToggleSelect(item.domain)} + aria-label={`Выбрать ${item.domain}`} /> {item.domain}{renderCommunityBadge(item.community)} +
+ + {item.domain} +
+
+
+ {renderCommunityBadge(item.community)} +
+
- {editingDomain === item.domain ? ( - <> - handleEditKeyDown(e, item.domain)} - className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} - /> - - - - ) : ( - <> - - - - )} +
+ {editingDomain === item.domain ? ( + <> + handleEditKeyDown(e, item.domain)} + className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} + style={{ width: '150px' }} + /> + + + + ) : ( + <> + + + + )} +
- - - {paginatedItems.map((item) => ( - - + {paginatedItems.map((item, idx) => ( + +
handleSort('ipRange')}> - IP-диапазон - {sortField === 'ipRange' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} + handleSort('ipRange')}> +
+ + IP-диапазон + {sortField === 'ipRange' && ( + + {sortOrder === 'asc' ? : } + + )} +
handleSort('community')}> - Community - {sortField === 'community' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} + handleSort('community')}> +
+ + Community + {sortField === 'community' && ( + + {sortOrder === 'asc' ? : } + + )} +
{item.ipRange}
+
+ + {item.ipRange} +
+
{renderCommunityBadge(item.community)} {editingIp === item.ipRange ? ( @@ -939,11 +1028,12 @@ function IPRangesManager() { value={editingValue} onChange={setEditingValue} communities={communities} - className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} + className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} + style={{ width: '150px' }} onKeyDown={(e) => handleEditKeyDown(e, item.ipRange)} /> - - + + ) : ( <> @@ -960,48 +1050,55 @@ function IPRangesManager() { )} {/* Пагинация */} {totalPages > 1 && ( -
-
- Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || paginatedItems.length} -
-
    -
  • - -
  • -
  • - -
  • - {(() => { - const pages = []; - const maxToShow = 7; // 1 .. 5 .. N - let start = Math.max(1, currentPage - 2); - let end = Math.min(totalPages, currentPage + 2); - if (currentPage <= 3) { end = Math.min(totalPages, 5); } - if (currentPage >= totalPages - 2) { start = Math.max(1, totalPages - 4); } - if (start > 1) pages.push('start-ellipsis'); - for (let p = start; p <= end; p++) pages.push(p); - if (end < totalPages) pages.push('end-ellipsis'); - const renderItem = (p) => { - if (p === 'start-ellipsis' || p === 'end-ellipsis') { - return ( +
    +
    +
    + Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || filtered.length} +
    +
      +
    • + +
    • +
    • + +
    • + {(() => { + const pages = []; + let start = Math.max(1, currentPage - 2); + let end = Math.min(totalPages, currentPage + 2); + if (currentPage <= 3) end = Math.min(totalPages, 5); + if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4); + if (start > 1) pages.push('start-ellipsis'); + for (let p = start; p <= end; p++) pages.push(p); + if (end < totalPages) pages.push('end-ellipsis'); + return pages.map((p) => ( + p === 'start-ellipsis' || p === 'end-ellipsis' ? (
    • - ); - } - return ( -
    • - -
    • - ); - }; - return pages.map(renderItem); - })()} -
    • - -
    • -
    • - -
    • -
    + ) : ( +
  • + +
  • + ) + )); + })()} +
  • + +
  • +
  • + +
  • +
+
)} diff --git a/frontend/src/index.css b/frontend/src/index.css index 6248b3c..7785fd1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -216,4 +216,140 @@ button:focus-visible, opacity: 1; transform: translate(-50%, -50%) scale(1.05); } +} + +/* ===== Animations ===== */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-in { + animation: fadeIn 0.4s ease both; +} + +/* ===== Statistics Cards ===== */ +.card-sm .avatar { + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--tblr-border-radius); +} + +.card-sm .h2 { + font-size: 1.5rem; + font-weight: 600; + line-height: 1.2; +} + +/* ===== Form Improvements ===== */ +.form-label.required::after { + content: ' *'; + color: var(--tblr-danger); +} + +.input-icon { + position: relative; +} + +.input-icon .input-icon-addon { + position: absolute; + left: 0.5rem; + top: 50%; + transform: translateY(-50%); + z-index: 1; + color: var(--tblr-muted); +} + +.input-icon .form-control, +.input-icon .form-select { + padding-left: 2.5rem; +} + +.input-icon .form-control:focus ~ .input-icon-addon, +.input-icon .form-select:focus ~ .input-icon-addon { + color: var(--tblr-primary); +} + +/* Кнопка очистки поиска */ +.input-icon .btn-icon { + position: absolute; + right: 0.25rem; + top: 50%; + transform: translateY(-50%); + z-index: 2; +} + +/* ===== Table Improvements ===== */ +.table thead th { + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; + color: var(--tblr-muted); + border-bottom: 2px solid var(--tblr-border-color); +} + +.table tbody tr { + border-bottom: 1px solid var(--tblr-border-color); +} + +.table tbody tr:last-child { + border-bottom: none; +} + +.table tbody tr.table-active { + background-color: rgba(32, 107, 196, 0.1); +} + +.user-select-none { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* ===== Pagination Improvements ===== */ +.pagination .page-link { + min-width: 2.5rem; + text-align: center; + transition: all 0.2s ease; +} + +.pagination .page-item.active .page-link { + font-weight: 600; +} + +/* ===== Card Header Improvements ===== */ +.card-header.bg-primary-lt { + background-color: rgba(32, 107, 196, 0.1); + border-bottom: 1px solid rgba(32, 107, 196, 0.2); +} + +/* ===== Responsive Improvements ===== */ +@media (max-width: 768px) { + .card-sm .avatar { + width: 2.5rem; + height: 2.5rem; + } + + .card-sm .h2 { + font-size: 1.25rem; + } + + .table-responsive { + font-size: 0.875rem; + } + + .btn-group { + flex-wrap: wrap; + } } \ No newline at end of file