From 730017405c5c0ff8aaab794360b18c95bd3e0595 Mon Sep 17 00:00:00 2001 From: shats Date: Wed, 27 Aug 2025 19:48:58 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=97=D0=B0=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=20=D0=B1=D0=B5=D0=B9=D0=B4=D0=B6=D0=B5=D0=B9=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D1=81=D1=82=D0=B2=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20CommunityBadge=20=D0=B2=20=D0=BC=D0=B5=D0=BD=D0=B5=D0=B4?= =?UTF-8?q?=D0=B6=D0=B5=D1=80=D0=B0=D1=85=20ASNs,=20Domains=20=D0=B8=20IPR?= =?UTF-8?q?anges,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B8=D0=B2=20=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20=D0=B8=20=D1=83=D0=BD=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=B2=20=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2=20CSV=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=83=D1=81=D1=82=D1=8B=D1=85=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D1=8D=D1=82?= =?UTF-8?q?=D0=B8=D1=85=20=D0=BC=D0=B5=D0=BD=D0=B5=D0=B4=D0=B6=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=85,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BE=D0=BF=D1=8B=D1=82?= =?UTF-8?q?.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BE=D0=BA?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B5=20=D1=83=D0=B4=D0=BE=D0=B1=D0=BD=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/ASNsNewManager.jsx | 81 ++++++----------- frontend/src/DomainsNewManager.jsx | 81 ++++++----------- frontend/src/IPRangesManager.jsx | 90 +++++++------------ frontend/src/components/CommunityBadge.jsx | 21 +++++ frontend/src/components/ConfirmDialog.jsx | 6 +- frontend/src/components/EmptyState.jsx | 6 +- frontend/src/components/PageHeaderActions.jsx | 7 +- frontend/src/index.css | 53 ++++++++++- 8 files changed, 171 insertions(+), 174 deletions(-) create mode 100644 frontend/src/components/CommunityBadge.jsx 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 && (