diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index 99f3b08..a0e7308 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import api from './lib/api.js'; import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx'; +import CommunityBadge from './components/CommunityBadge.jsx'; import { IconPlus, IconSearch, @@ -78,17 +79,7 @@ function ASNsNewManager() { // Подгружаем имена ASN из кэша/внешних API (эффект будет размещен ниже после определения paginatedItems) - const renderCommunityBadge = (value) => { - const v = String(value ?? '').trim(); - if (!v) return ; - const meta = communities.find(c => c.value === v); - const color = (meta?.color || 'blue').toLowerCase().replace(/[^a-z-]/g, ''); - const cls = `badge bg-${color}-lt text-${color}`; - const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : ''; - return ( - {v} - ); - }; + const renderCommunityBadge = (value) => (); const isValidAsn = (value) => /^[0-9]+$/.test(String(value).trim()); // Допускаем как числовые, так и строковые (AS:NNN) community @@ -635,7 +626,18 @@ function ASNsNewManager() { ) : paginatedItems.length === 0 ? ( - Импорт} /> + Импорт} + secondaryAction={} + /> ) : (
@@ -748,50 +750,17 @@ function ASNsNewManager() {
- {/* Delete Confirmation Modal */} - {showDeleteModal && ( -
-
-
- -
-
- -

Удалить ASN?

-
- Вы уверены, что хотите удалить "{itemToDelete?.asn}"? Это действие необратимо. -
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- )} + setShowDeleteModal(false)} + /> { - const v = String(value ?? '').trim(); - if (!v) return ; - const meta = communities.find(c => c.value === v); - const color = (meta?.color || 'blue').toLowerCase().replace(/[^a-z-]/g, ''); - const cls = `badge bg-${color}-lt text-${color}`; - const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : ''; - return ( - {v} - ); - }; + const renderCommunityBadge = (value) => (); const isValidDomain = (value) => { const v = String(value).trim().toLowerCase(); @@ -623,7 +614,18 @@ function DomainsNewManager() { ) : paginatedItems.length === 0 ? ( - Импорт} /> + Импорт} + secondaryAction={} + /> ) : (
@@ -726,50 +728,17 @@ function DomainsNewManager() {
- {/* Delete Confirmation Modal */} - {showDeleteModal && ( -
-
-
- -
-
- -

Удалить домен?

-
- Вы уверены, что хотите удалить "{itemToDelete?.domain}"? Это действие необратимо. -
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- )} + setShowDeleteModal(false)} + /> {/* Confirm Save Modal */} { - const v = String(value ?? '').trim(); - if (!v) return ; - const meta = communities.find(c => c.value === v); - const color = (meta?.color || 'blue').toLowerCase().replace(/[^a-z-]/g, ''); - const cls = `badge bg-${color}-lt text-${color}`; - const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : ''; - return ( - {v} - ); - }; + const renderCommunityBadge = (value) => (); const isValidIPv4 = (ip) => { const octets = ip.split('.'); @@ -364,6 +355,21 @@ function IPRangesManager() { setImportOpen(false); }; + // Шаблон CSV для пустого состояния + const downloadSampleCsv = () => { + const header = ['ipRange','community']; + const lines = [header, ['192.168.1.0/24', '65000:100']] + .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([lines], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'ip_ranges_sample.csv'; + a.click(); + URL.revokeObjectURL(url); + }; + // Сортировка const sortedItems = [...items].sort((a, b) => { let valA = a[sortField] || ''; @@ -878,7 +884,12 @@ function IPRangesManager() { ) : paginatedItems.length === 0 ? ( - Импорт} /> + Импорт} + secondaryAction={} + /> ) : (
@@ -986,50 +997,17 @@ function IPRangesManager() {
- {/* Delete Confirmation Modal */} - {showDeleteModal && ( -
-
-
- -
-
- -

Удалить IP-диапазон?

-
- Вы уверены, что хотите удалить "{itemToDelete?.ipRange}"? Это действие необратимо. -
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- )} + setShowDeleteModal(false)} + /> —; + const meta = communities.find(c => String(c?.value) === v); + const color = normalizeTablerColor(meta?.color || 'blue'); + const cls = `badge bg-${color}-lt text-${color}`; + const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${Array.isArray(meta.tags) && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : ''; + return ( + {v} + ); +} + +export default CommunityBadge; + + diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx index 01ac662..d83838e 100644 --- a/frontend/src/components/ConfirmDialog.jsx +++ b/frontend/src/components/ConfirmDialog.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react' -export default function ConfirmDialog({ open, title, message, confirmText = 'Подтвердить', cancelText = 'Отмена', onConfirm, onCancel }) { +export default function ConfirmDialog({ open, title, message, confirmText = 'Подтвердить', cancelText = 'Отмена', onConfirm, onCancel, destructive = false, size = 'md' }) { const ref = useRef(null) useEffect(() => { if (open && ref.current) { @@ -10,7 +10,7 @@ export default function ConfirmDialog({ open, title, message, confirmText = 'П if (!open) return null return (
{ if (e.key === 'Escape') onCancel?.() }}> -
+
{ if (e.key === 'Tab') { const focusable = ref.current?.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') @@ -30,7 +30,7 @@ export default function ConfirmDialog({ open, title, message, confirmText = 'П
- +
diff --git a/frontend/src/components/EmptyState.jsx b/frontend/src/components/EmptyState.jsx index a88e604..839c79b 100644 --- a/frontend/src/components/EmptyState.jsx +++ b/frontend/src/components/EmptyState.jsx @@ -5,6 +5,7 @@ function EmptyState({ title = 'Нет данных', description, action, + secondaryAction, }) { return (
@@ -17,9 +18,12 @@ function EmptyState({ {description}

)} - {action && ( + {(action || secondaryAction) && (
{action} + {secondaryAction && ( + {secondaryAction} + )}
)}
diff --git a/frontend/src/components/PageHeaderActions.jsx b/frontend/src/components/PageHeaderActions.jsx index 7b62206..584ef7c 100644 --- a/frontend/src/components/PageHeaderActions.jsx +++ b/frontend/src/components/PageHeaderActions.jsx @@ -33,7 +33,7 @@ function PageHeaderActions({ onOnlineUpdate, onBackgroundUpdate, }) { - const secondaryButtons = Boolean(onImport || onExport || onClear || onClearCommunities || onHistory); + const secondaryButtons = Boolean(onPreview || onImport || onExport || onClear || onClearCommunities || onHistory); // Рендер плейсхолдеров во время загрузки/перезагрузки страницы if (loading) { const showUpdateGroup = Boolean(onBackgroundUpdate || onOnlineUpdate); @@ -176,6 +176,11 @@ function PageHeaderActions({ align="left" buttonContent={<> Ещё} > + {onPreview && ( + + )} {onRefresh && (