diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index 6c96b65..1d54099 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -19,7 +19,8 @@ import { IconFileText, IconFilter, IconArrowUp, - IconArrowDown + IconArrowDown, + IconCopy } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; @@ -37,6 +38,8 @@ 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 BulkActionsBar from './components/BulkActionsBar.jsx'; +import Tooltip from './components/Tooltip.jsx'; import { getAsnName, getAsnNameSync } from './lib/asn.js'; const API_URL = '/api'; @@ -67,6 +70,7 @@ function ASNsNewManager() { const editInputRef = useRef(null); const pageSize = 10; const [asnNameMap, setAsnNameMap] = useState({}); + const [selectedItems, setSelectedItems] = useState(new Set()); // Справочник community для подсказок const [communities, setCommunities] = useState([]); @@ -161,6 +165,65 @@ function ASNsNewManager() { const handleDeleteItem = (asnToDelete) => { setItems(items.filter(i => i.asn !== asnToDelete)); + setSelectedItems(prev => { + const next = new Set(prev); + next.delete(asnToDelete); + return next; + }); + }; + + // Функции для работы с выбранными элементами + const toggleSelectItem = (asn) => { + setSelectedItems(prev => { + const next = new Set(prev); + if (next.has(asn)) { + next.delete(asn); + } else { + next.add(asn); + } + return next; + }); + }; + + const selectAll = () => { + setSelectedItems(new Set(paginatedItems.map(i => i.asn))); + }; + + const deselectAll = () => { + setSelectedItems(new Set()); + }; + + const handleBulkDelete = () => { + if (selectedItems.size === 0) return; + const count = selectedItems.size; + setItems(items.filter(i => !selectedItems.has(i.asn))); + setSelectedItems(new Set()); + window.notify?.success?.(`Удалено ${count} записей`); + }; + + const handleBulkExport = () => { + if (selectedItems.size === 0) return; + const selected = items.filter(i => selectedItems.has(i.asn)); + const header = ['asn', 'community']; + const csv = [header, ...selected.map(i => [i.asn, i.community])] + .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `asns_selected_${new Date().toISOString().split('T')[0]}.csv`; + a.click(); + URL.revokeObjectURL(url); + window.notify?.success?.(`Экспортировано ${selectedItems.size} записей`); + }; + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text).then(() => { + window.notify?.success?.('Скопировано в буфер обмена'); + }).catch(() => { + window.notify?.error?.('Не удалось скопировать'); + }); }; const confirmDelete = (item) => { @@ -491,66 +554,6 @@ function ASNsNewManager() { /> )} /> - {/* Статистические карточки */} -
| + 0 && paginatedItems.every(i => selectedItems.has(i.asn))} + onChange={(e) => { + if (e.target.checked) { + selectAll(); + } else { + deselectAll(); + } + }} + title="Выбрать все на странице" + /> + | handleSort('asn')}>
|
- + | Действия | |||||
|---|---|---|---|---|---|---|---|---|
| + toggleSelectItem(item.asn)} + onClick={(e) => e.stopPropagation()} + /> + |
|
@@ -819,13 +869,24 @@ function ASNsNewManager() {
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
style={{ width: '150px' }}
/>
-
-
+ |
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx
index e17fe3f..0863cb0 100644
--- a/frontend/src/DomainsNewManager.jsx
+++ b/frontend/src/DomainsNewManager.jsx
@@ -23,7 +23,8 @@ import {
IconTrendingUp,
IconUsers,
IconArrowUp,
- IconArrowDown
+ IconArrowDown,
+ IconCopy
} from '@tabler/icons-react';
import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
import PageHeader from './components/PageHeader.jsx';
@@ -44,6 +45,7 @@ import AccordionCard from './components/AccordionCard.jsx';
import SavedFilters from './components/SavedFilters.jsx';
import BulkActionsBar from './components/BulkActionsBar.jsx';
import FormField from './components/FormField.jsx';
+import Tooltip from './components/Tooltip.jsx';
import { useToast } from './components/ToastContainer.jsx';
const API_URL = '/api';
@@ -171,6 +173,19 @@ function DomainsNewManager() {
const handleDeleteItem = (domainNameToDelete) => {
setItems(items.filter(i => i.domain !== domainNameToDelete));
+ setSelectedDomains(prev => {
+ const next = new Set(prev);
+ next.delete(domainNameToDelete);
+ return next;
+ });
+ };
+
+ const copyToClipboard = (text) => {
+ navigator.clipboard.writeText(text).then(() => {
+ toast.success('Скопировано в буфер обмена');
+ }).catch(() => {
+ toast.error('Не удалось скопировать');
+ });
};
const confirmDelete = (item) => {
@@ -448,10 +463,11 @@ function DomainsNewManager() {
const handleBulkDelete = () => {
if (selectedDomains.size === 0) return;
+ const count = selectedDomains.size;
const newItems = items.filter(i => !selectedDomains.has(i.domain));
setItems(newItems);
setSelectedDomains(new Set());
- toast.success(`Удалено ${selectedDomains.size} доменов`);
+ toast.success(`Удалено ${count} доменов`);
};
const handleBulkExport = () => {
@@ -536,96 +552,6 @@ function DomainsNewManager() {
)}
/>
- {/* Статистические карточки */}
- + | Действия | ||||
|
|
@@ -952,42 +888,55 @@ function DomainsNewManager() {
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
style={{ width: '150px' }}
/>
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- Всего диапазонов
-
- {items.length}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Отфильтровано
-
- {filtered.length}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Уникальных Community
-
- {allCommunities.length}
-
-
{/* Classic Add New Item Card (improved) */}
@@ -919,54 +928,81 @@ function IPRangesManager() {
+ {/* BulkActionsBar */}
+
-
+ Список IP-диапазонов {items.length}-
- {/* Фильтр по community */}
-
- {/* Поиск */}
-
-
-
{/* Мета-информация будет показана внизу карточки */}
@@ -986,6 +1022,21 @@ function IPRangesManager() {
+
-
+ {/* Фильтр по community */}
+
+ {/* Поиск */}
+
-
+
+
-
+
|