From e6878a5b08d6b458a61ae7e304f176bbde67e2c4 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Thu, 14 Aug 2025 14:21:14 +0700 Subject: [PATCH] feat: Add ConfirmDialog for clearing community fields in ASNs, Domains, and IPRanges managers with appropriate state management and UI integration --- frontend/src/ASNsNewManager.jsx | 13 ++++ frontend/src/CommunitiesManager.jsx | 67 +++++++++++++++++-- frontend/src/DomainsNewManager.jsx | 13 ++++ frontend/src/IPRangesManager.jsx | 13 ++++ frontend/src/components/PageHeaderActions.jsx | 10 ++- 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index dd44104..1f914a2 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -21,6 +21,7 @@ import LockBanner from './components/LockBanner.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; import PageHeaderActions from './components/PageHeaderActions.jsx'; +import ConfirmDialog from './components/ConfirmDialog.jsx'; import WsUpdateModal from './components/WsUpdateModal.jsx'; import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; @@ -204,6 +205,7 @@ function ASNsNewManager() { const [confirmSaveOpen, setConfirmSaveOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [wsOpen, setWsOpen] = useState(false); + const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false); const handlePreviewDiff = () => { const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community)) @@ -405,6 +407,8 @@ function ASNsNewManager() { disableExport={items.length === 0} onClear={clearInvalid} disableClear={items.length === 0} + onClearCommunities={() => setClearCommunitiesOpen(true)} + disableClearCommunities={items.length === 0} onSave={handleSaveChanges} disableSave={loading} onHistory={() => setHistoryOpen(true)} @@ -688,6 +692,15 @@ function ASNsNewManager() { url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'} onClose={() => setWsOpen(false)} /> + { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }} + onCancel={() => setClearCommunitiesOpen(false)} + /> {/* datalist больше не нужен, т.к. используем кастомный автокомплит */} ); diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index d5a1437..d23c5fc 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -40,8 +40,56 @@ function CommunitiesManager() { const fetchItems = async () => { setLoading(true); try { - const res = await api.get(`/communities`); - setItems(res.data); + const [dictRes, asnsRes, ipRes, domRes] = await Promise.all([ + api.get(`/communities`), + api.get(`/asns`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), + api.get(`/ip-ranges`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), + api.get(`/domains-new`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), + ]); + const dict = Array.isArray(dictRes.data) ? dictRes.data : []; + const asnsPayload = Array.isArray(asnsRes.data?.items) ? asnsRes.data.items : (Array.isArray(asnsRes.data) ? asnsRes.data : []); + const ipPayload = Array.isArray(ipRes.data?.items) ? ipRes.data.items : (Array.isArray(ipRes.data) ? ipRes.data : []); + const domPayload = Array.isArray(domRes.data?.items) ? domRes.data.items : (Array.isArray(domRes.data) ? domRes.data : []); + + const usedCommunities = new Set(); + // ASNs: fields domain/type + for (const it of asnsPayload) { + const v = String(it.type ?? '').trim(); + if (v) usedCommunities.add(v); + } + // IP ranges: fields ipRange/community + for (const it of ipPayload) { + const v = String(it.community ?? '').trim(); + if (v) usedCommunities.add(v); + } + // Domains: fields domain/community + for (const it of domPayload) { + const v = String(it.community ?? '').trim(); + if (v) usedCommunities.add(v); + } + + const map = new Map(); + // put dictionary items first + for (const d of dict) { + const value = String(d.value || '').trim(); + if (!value) continue; + map.set(value, { + value, + name: String(d.name || ''), + description: String(d.description || ''), + tags: Array.isArray(d.tags) ? d.tags : [], + gatewayDefault: String(d.gatewayDefault || ''), + color: String(d.color || ''), + _external: false, + }); + } + // add unknown ones from usage + for (const v of usedCommunities) { + if (!map.has(v)) { + map.set(v, { value: v, name: '', description: '', tags: [], gatewayDefault: '', color: '', _external: true }); + } + } + setItems(Array.from(map.values())); setError(''); } catch (e) { console.error('Error fetching communities:', e); @@ -54,7 +102,9 @@ function CommunitiesManager() { const saveAll = async (data) => { setLoading(true); try { - await api.post(`/communities`, { communities: data }); + // не отправляем служебное поле _external + const payload = data.map(({ _external, ...rest }) => rest); + await api.post(`/communities`, { communities: payload }); setSuccess('Справочник сохранён!'); setTimeout(() => setSuccess(''), 3000); } catch (e) { @@ -256,7 +306,7 @@ function CommunitiesManager() {
- + @@ -271,12 +321,17 @@ function CommunitiesManager() { ) : paginated.length === 0 ? ( ) : paginated.map((item) => ( - +
changeSort('value')}>Value {sortField==='value' && {sortOrder==='asc'?'▲':'▼'}} changeSort('name')}>Название {sortField==='name' && {sortOrder==='asc'?'▲':'▼'}} Описание
Нет записей
{editingValue === item.value ? ( setEditingDraft({...editingDraft, value:e.target.value})} ref={editRef} /> ) : ( - {item.value} + <> + {item.value} + {item._external && ( + не в справ. + )} + )} diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index dce961f..a42bf45 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -21,6 +21,7 @@ import LockBanner from './components/LockBanner.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; import PageHeaderActions from './components/PageHeaderActions.jsx'; +import ConfirmDialog from './components/ConfirmDialog.jsx'; import WsUpdateModal from './components/WsUpdateModal.jsx'; import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; @@ -208,6 +209,7 @@ function DomainsNewManager() { const [confirmSaveOpen, setConfirmSaveOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [wsOpen, setWsOpen] = useState(false); + const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false); const handlePreviewDiff = () => { const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community)) @@ -412,6 +414,8 @@ function DomainsNewManager() { disableExport={items.length === 0} onClear={clearInvalid} disableClear={items.length === 0} + onClearCommunities={() => setClearCommunitiesOpen(true)} + disableClearCommunities={items.length === 0} onSave={handleSaveChanges} disableSave={loading} onHistory={() => setHistoryOpen(true)} @@ -697,6 +701,15 @@ function DomainsNewManager() { url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'} onClose={() => setWsOpen(false)} /> + { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }} + onCancel={() => setClearCommunitiesOpen(false)} + /> {/* Diff Modal */} {showDiff && (
diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index ab60938..0326e79 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -21,6 +21,7 @@ import LockBanner from './components/LockBanner.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import S3MetaBar from './components/S3MetaBar.jsx'; import PageHeaderActions from './components/PageHeaderActions.jsx'; +import ConfirmDialog from './components/ConfirmDialog.jsx'; import WsUpdateModal from './components/WsUpdateModal.jsx'; import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; @@ -219,6 +220,7 @@ function IPRangesManager() { const [confirmSaveOpen, setConfirmSaveOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [wsOpen, setWsOpen] = useState(false); + const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false); const handlePreviewDiff = () => { const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community)) @@ -421,6 +423,8 @@ function IPRangesManager() { disableExport={items.length === 0} onClear={clearInvalid} disableClear={items.length === 0} + onClearCommunities={() => setClearCommunitiesOpen(true)} + disableClearCommunities={items.length === 0} onSave={handleSaveChanges} disableSave={loading} onHistory={() => setHistoryOpen(true)} @@ -709,6 +713,15 @@ function IPRangesManager() { url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'} onClose={() => setWsOpen(false)} /> + { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }} + onCancel={() => setClearCommunitiesOpen(false)} + /> {/* Diff Modal */} {showDiff && (
diff --git a/frontend/src/components/PageHeaderActions.jsx b/frontend/src/components/PageHeaderActions.jsx index cb59bbc..17e8f8d 100644 --- a/frontend/src/components/PageHeaderActions.jsx +++ b/frontend/src/components/PageHeaderActions.jsx @@ -4,7 +4,8 @@ import { IconDownload, IconDeviceFloppy, IconPlayerPlay, - IconBolt + IconBolt, + IconEraser } from '@tabler/icons-react'; import PortalDropdown from './PortalDropdown.jsx'; @@ -19,6 +20,8 @@ function PageHeaderActions({ disableExport = false, onClear, disableClear = false, + onClearCommunities, + disableClearCommunities = false, onSave, disableSave = false, onHistory, @@ -90,6 +93,11 @@ function PageHeaderActions({ Очистить )} + {onClearCommunities && ( + + )} {onSave && (