From 3358b8daed690098b750884390bd1a14fcda06fd Mon Sep 17 00:00:00 2001 From: shats Date: Wed, 27 Aug 2025 16:57:40 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BE=D0=B1=20=D1=83=D1=81=D0=BF=D0=B5?= =?UTF-8?q?=D1=88=D0=BD=D1=8B=D1=85=20=D0=BC=D1=83=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=D1=85=20=D0=B8=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=B3=D0=BE=D1=80=D1=8F=D1=87=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D1=88=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B2=20=D0=BC=D0=B5=D0=BD=D0=B5=D0=B4=D0=B6=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=85=20ASNs,=20Domains=20=D0=B8=20IPRanges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/ASNsNewManager.jsx | 34 +++++++++++++--------- frontend/src/DomainsNewManager.jsx | 34 +++++++++++++--------- frontend/src/IPRangesManager.jsx | 32 +++++++++++--------- frontend/src/components/ErrorAlert.jsx | 33 +++++++++++++++++++++ frontend/src/components/NotifyProvider.jsx | 10 +++++++ 5 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/ErrorAlert.jsx diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index b17aebf..c9fcb8b 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -17,7 +17,6 @@ import { IconClock, IconFileText } from '@tabler/icons-react'; -import LockBanner from './components/LockBanner.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; import PageHeaderActions from './components/PageHeaderActions.jsx'; @@ -25,6 +24,8 @@ import ConfirmDialog from './components/ConfirmDialog.jsx'; import WsUpdateModal from './components/WsUpdateModal.jsx'; import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; +import ErrorAlert from './components/ErrorAlert.jsx'; +import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; const API_URL = '/api'; @@ -261,6 +262,7 @@ function ASNsNewManager() { setItems(unique); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); + notifyMutationSuccess('Изменения сохранены'); } catch (error) { console.error('Error saving changes:', error); setError('Не удалось сохранить изменения.'); @@ -370,6 +372,18 @@ function ASNsNewManager() { } }, [editingAsn]); + // Глобальные хоткеи: Ctrl+S — сохранить изменения + useEffect(() => { + const onKey = (e) => { + if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { + e.preventDefault(); + if (!loading) handleSaveChanges(); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]); + const handleEditKeyDown = (e, asn) => { if (e.key === 'Enter') handleSaveEdit(asn); if (e.key === 'Escape') handleCancelEdit(); @@ -378,17 +392,7 @@ function ASNsNewManager() { return ( <> {error && ( -
-
-
- -
-
- {error} -
-
- -
+ setError('')} /> )} {success && ( @@ -434,7 +438,7 @@ function ASNsNewManager() { if (!res.ok || data.ok === false) { throw new Error(data?.message || `HTTP ${res.status}`); } - window.notify?.success('Фоновое обновление запущено', data); + notifyMutationSuccess('Фоновое обновление запущено', data); } catch (e) { window.notify?.error('Не удалось запустить фоновое обновление', String(e)); } @@ -445,7 +449,6 @@ function ASNsNewManager() {
- {/* Add New ASN Card */}
@@ -466,6 +469,7 @@ function ASNsNewManager() { onChange={(e) => setNewItem({ ...newItem, asn: e.target.value })} /> {newInvalid.asn &&
Только цифры
} +
Формат: только цифры, например 12345
@@ -477,6 +481,7 @@ function ASNsNewManager() { className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} /> {newInvalid.community &&
Только цифры
} +
Формат: N или N:N (например 65000:100)
{(() => { const match = communities.find(c => c.value === String(newItem.community).trim()); if (!match) return null; @@ -582,6 +587,7 @@ function ASNsNewManager() { value={editingValue} onChange={setEditingValue} communities={communities} + onKeyDown={(e) => handleEditKeyDown(e, item.asn)} className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} /> diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index fba01bf..5f4d823 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -17,7 +17,6 @@ import { IconClock, IconFileText } from '@tabler/icons-react'; -import LockBanner from './components/LockBanner.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; import PageHeaderActions from './components/PageHeaderActions.jsx'; @@ -25,6 +24,8 @@ import ConfirmDialog from './components/ConfirmDialog.jsx'; import WsUpdateModal from './components/WsUpdateModal.jsx'; import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; +import ErrorAlert from './components/ErrorAlert.jsx'; +import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; const API_URL = '/api'; @@ -265,6 +266,7 @@ function DomainsNewManager() { setItems(unique); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); + notifyMutationSuccess('Изменения сохранены'); } catch (error) { console.error('Error saving changes:', error); setError('Не удалось сохранить изменения.'); @@ -375,6 +377,18 @@ function DomainsNewManager() { } }, [editingDomain]); + // Глобальные хоткеи: Ctrl+S — сохранить изменения + useEffect(() => { + const onKey = (e) => { + if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { + e.preventDefault(); + if (!loading) handleSaveChanges(); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]); + const handleEditKeyDown = (e, domainName) => { if (e.key === 'Enter') handleSaveEdit(domainName); if (e.key === 'Escape') handleCancelEdit(); @@ -383,17 +397,7 @@ function DomainsNewManager() { return ( <> {error && ( -
-
-
- -
-
- {error} -
-
- -
+ setError('')} /> )} {success && ( @@ -439,7 +443,7 @@ function DomainsNewManager() { if (!res.ok || data.ok === false) { throw new Error(data?.message || `HTTP ${res.status}`); } - window.notify?.success('Фоновое обновление запущено', data); + notifyMutationSuccess('Фоновое обновление запущено', data); } catch (e) { window.notify?.error('Не удалось запустить фоновое обновление', String(e)); } @@ -450,7 +454,6 @@ function DomainsNewManager() {
- {/* Add New Item Card */}
@@ -472,6 +475,7 @@ function DomainsNewManager() { /> {newInvalid.domain &&
Неверный формат домена
}
Без http/https, только доменное имя
+
Формат: sub.domain.tld (например foo.example.org)
@@ -483,6 +487,7 @@ function DomainsNewManager() { className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} /> {newInvalid.community &&
Только цифры
} +
Формат: N или N:N (например 65000:100)
{(() => { const match = communities.find(c => c.value === String(newItem.community).trim()); if (!match) return null; @@ -588,6 +593,7 @@ function DomainsNewManager() { value={editingValue} onChange={setEditingValue} communities={communities} + onKeyDown={(e) => handleEditKeyDown(e, item.domain)} className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} /> diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index c6690c8..9f9618c 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -17,7 +17,6 @@ import { IconClock, IconFileText } from '@tabler/icons-react'; -import LockBanner from './components/LockBanner.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; import PageHeaderActions from './components/PageHeaderActions.jsx'; @@ -25,6 +24,8 @@ import ConfirmDialog from './components/ConfirmDialog.jsx'; import WsUpdateModal from './components/WsUpdateModal.jsx'; import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; +import ErrorAlert from './components/ErrorAlert.jsx'; +import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; const API_URL = '/api'; @@ -282,6 +283,7 @@ function IPRangesManager() { setItems(unique); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); + notifyMutationSuccess('Изменения сохранены'); } catch (error) { console.error('Error saving changes:', error); setError('Не удалось сохранить изменения.'); @@ -390,6 +392,17 @@ function IPRangesManager() { editInputRef.current.focus(); } }, [editingIp]); + // Глобальные хоткеи: Ctrl+S — сохранить изменения + useEffect(() => { + const onKey = (e) => { + if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { + e.preventDefault(); + if (!loading) handleSaveChanges(); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]); const handleEditKeyDown = (e, ipRange) => { if (e.key === 'Enter') handleSaveEdit(ipRange); @@ -638,17 +651,7 @@ function IPRangesManager() { return ( <> {error && ( -
-
-
- -
-
- {error} -
-
- -
+ setError('')} /> )} {success && ( @@ -694,7 +697,7 @@ function IPRangesManager() { if (!res.ok || data.ok === false) { throw new Error(data?.message || `HTTP ${res.status}`); } - window.notify?.success('Фоновое обновление запущено', data); + notifyMutationSuccess('Фоновое обновление запущено', data); } catch (e) { window.notify?.error('Не удалось запустить фоновое обновление', String(e)); } @@ -705,7 +708,6 @@ function IPRangesManager() {
- {/* Add New Item Card */}
@@ -726,6 +728,7 @@ function IPRangesManager() { onChange={(e) => setNewItem({ ...newItem, ipRange: e.target.value })} /> {newInvalid.ipRange &&
Некорректный CIDR
} +
Формат: A.B.C.D/Маска, например 10.0.0.0/24
@@ -848,6 +851,7 @@ function IPRangesManager() { onChange={setEditingValue} communities={communities} className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} + onKeyDown={(e) => handleEditKeyDown(e, item.ipRange)} /> diff --git a/frontend/src/components/ErrorAlert.jsx b/frontend/src/components/ErrorAlert.jsx new file mode 100644 index 0000000..7c86202 --- /dev/null +++ b/frontend/src/components/ErrorAlert.jsx @@ -0,0 +1,33 @@ +import { IconAlertTriangle, IconX } from '@tabler/icons-react'; + +function ErrorAlert({ message, details, onClose }) { + if (!message) return null; + return ( +
+
+
+ +
+
+ {String(message)} + {details ? ( +
+ Показать детали +
+                {typeof details === 'string' ? details : JSON.stringify(details, null, 2)}
+              
+
+ ) : null} +
+
+ +
+ ); +} + +export default ErrorAlert; + + + diff --git a/frontend/src/components/NotifyProvider.jsx b/frontend/src/components/NotifyProvider.jsx index 108e564..714baff 100644 --- a/frontend/src/components/NotifyProvider.jsx +++ b/frontend/src/components/NotifyProvider.jsx @@ -88,6 +88,16 @@ export function useNotify() { return useContext(NotifyContext); } +// Единый тост-успех для мутаций (POST/DELETE/PUT/PATCH) +export function notifyMutationSuccess(message, details) { + try { + const text = message || 'Операция выполнена'; + if (typeof window !== 'undefined' && window.notify?.success) { + window.notify.success(text, details); + } + } catch {} +} + function colorByType(type) { switch (type) { case 'success': return 'success';