feat: Add ConfirmDialog for clearing community fields in ASNs, Domains, and IPRanges managers with appropriate state management and UI integration
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 15m31s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 15m31s
This commit is contained in:
@@ -21,6 +21,7 @@ import LockBanner from './components/LockBanner.jsx';
|
|||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
|
import ConfirmDialog from './components/ConfirmDialog.jsx';
|
||||||
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
@@ -204,6 +205,7 @@ function ASNsNewManager() {
|
|||||||
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
|
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
|
||||||
const [historyOpen, setHistoryOpen] = useState(false);
|
const [historyOpen, setHistoryOpen] = useState(false);
|
||||||
const [wsOpen, setWsOpen] = useState(false);
|
const [wsOpen, setWsOpen] = useState(false);
|
||||||
|
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
|
||||||
|
|
||||||
const handlePreviewDiff = () => {
|
const handlePreviewDiff = () => {
|
||||||
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
|
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
|
||||||
@@ -405,6 +407,8 @@ function ASNsNewManager() {
|
|||||||
disableExport={items.length === 0}
|
disableExport={items.length === 0}
|
||||||
onClear={clearInvalid}
|
onClear={clearInvalid}
|
||||||
disableClear={items.length === 0}
|
disableClear={items.length === 0}
|
||||||
|
onClearCommunities={() => setClearCommunitiesOpen(true)}
|
||||||
|
disableClearCommunities={items.length === 0}
|
||||||
onSave={handleSaveChanges}
|
onSave={handleSaveChanges}
|
||||||
disableSave={loading}
|
disableSave={loading}
|
||||||
onHistory={() => setHistoryOpen(true)}
|
onHistory={() => setHistoryOpen(true)}
|
||||||
@@ -688,6 +692,15 @@ function ASNsNewManager() {
|
|||||||
url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'}
|
url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'}
|
||||||
onClose={() => setWsOpen(false)}
|
onClose={() => setWsOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={clearCommunitiesOpen}
|
||||||
|
title={'Очистить все community?'}
|
||||||
|
message={'Поле community будет очищено у всех записей в таблице. Это действие не сохраняет изменения автоматически.'}
|
||||||
|
confirmText={'Очистить'}
|
||||||
|
cancelText={'Отмена'}
|
||||||
|
onConfirm={() => { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }}
|
||||||
|
onCancel={() => setClearCommunitiesOpen(false)}
|
||||||
|
/>
|
||||||
{/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
|
{/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,8 +40,56 @@ function CommunitiesManager() {
|
|||||||
const fetchItems = async () => {
|
const fetchItems = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/communities`);
|
const [dictRes, asnsRes, ipRes, domRes] = await Promise.all([
|
||||||
setItems(res.data);
|
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('');
|
setError('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching communities:', e);
|
console.error('Error fetching communities:', e);
|
||||||
@@ -54,7 +102,9 @@ function CommunitiesManager() {
|
|||||||
const saveAll = async (data) => {
|
const saveAll = async (data) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.post(`/communities`, { communities: data });
|
// не отправляем служебное поле _external
|
||||||
|
const payload = data.map(({ _external, ...rest }) => rest);
|
||||||
|
await api.post(`/communities`, { communities: payload });
|
||||||
setSuccess('Справочник сохранён!');
|
setSuccess('Справочник сохранён!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -256,7 +306,7 @@ function CommunitiesManager() {
|
|||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="cursor-pointer" onClick={()=>changeSort('value')}>Value {sortField==='value' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
<th className="cursor-pointer" onClick={()=>changeSort('value')}>Value {sortField==='value' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
||||||
<th className="cursor-pointer" onClick={()=>changeSort('name')}>Название {sortField==='name' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
<th className="cursor-pointer" onClick={()=>changeSort('name')}>Название {sortField==='name' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
||||||
<th>Описание</th>
|
<th>Описание</th>
|
||||||
@@ -271,12 +321,17 @@ function CommunitiesManager() {
|
|||||||
) : paginated.length === 0 ? (
|
) : paginated.length === 0 ? (
|
||||||
<tr><td colSpan={6} className="text-center text-muted py-4">Нет записей</td></tr>
|
<tr><td colSpan={6} className="text-center text-muted py-4">Нет записей</td></tr>
|
||||||
) : paginated.map((item) => (
|
) : paginated.map((item) => (
|
||||||
<tr key={item.value} className={editingValue === item.value ? 'table-info' : ''}>
|
<tr key={item.value} className={editingValue === item.value ? 'table-info' : ''}>
|
||||||
<td>
|
<td>
|
||||||
{editingValue === item.value ? (
|
{editingValue === item.value ? (
|
||||||
<input className="form-control form-control-sm" value={editingDraft.value} onChange={(e)=>setEditingDraft({...editingDraft, value:e.target.value})} ref={editRef} />
|
<input className="form-control form-control-sm" value={editingDraft.value} onChange={(e)=>setEditingDraft({...editingDraft, value:e.target.value})} ref={editRef} />
|
||||||
) : (
|
) : (
|
||||||
<code className="text-blue">{item.value}</code>
|
<>
|
||||||
|
<code className="text-blue">{item.value}</code>
|
||||||
|
{item._external && (
|
||||||
|
<span className="badge bg-yellow-lt text-yellow ms-2" title="Не внесён в справочник">не в справ.</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import LockBanner from './components/LockBanner.jsx';
|
|||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
|
import ConfirmDialog from './components/ConfirmDialog.jsx';
|
||||||
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
@@ -208,6 +209,7 @@ function DomainsNewManager() {
|
|||||||
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
|
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
|
||||||
const [historyOpen, setHistoryOpen] = useState(false);
|
const [historyOpen, setHistoryOpen] = useState(false);
|
||||||
const [wsOpen, setWsOpen] = useState(false);
|
const [wsOpen, setWsOpen] = useState(false);
|
||||||
|
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
|
||||||
|
|
||||||
const handlePreviewDiff = () => {
|
const handlePreviewDiff = () => {
|
||||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||||
@@ -412,6 +414,8 @@ function DomainsNewManager() {
|
|||||||
disableExport={items.length === 0}
|
disableExport={items.length === 0}
|
||||||
onClear={clearInvalid}
|
onClear={clearInvalid}
|
||||||
disableClear={items.length === 0}
|
disableClear={items.length === 0}
|
||||||
|
onClearCommunities={() => setClearCommunitiesOpen(true)}
|
||||||
|
disableClearCommunities={items.length === 0}
|
||||||
onSave={handleSaveChanges}
|
onSave={handleSaveChanges}
|
||||||
disableSave={loading}
|
disableSave={loading}
|
||||||
onHistory={() => setHistoryOpen(true)}
|
onHistory={() => setHistoryOpen(true)}
|
||||||
@@ -697,6 +701,15 @@ function DomainsNewManager() {
|
|||||||
url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'}
|
url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'}
|
||||||
onClose={() => setWsOpen(false)}
|
onClose={() => setWsOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={clearCommunitiesOpen}
|
||||||
|
title={'Очистить все community?'}
|
||||||
|
message={'Поле community будет очищено у всех доменов. Это действие не сохраняет изменения автоматически.'}
|
||||||
|
confirmText={'Очистить'}
|
||||||
|
cancelText={'Отмена'}
|
||||||
|
onConfirm={() => { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }}
|
||||||
|
onCancel={() => setClearCommunitiesOpen(false)}
|
||||||
|
/>
|
||||||
{/* Diff Modal */}
|
{/* Diff Modal */}
|
||||||
{showDiff && (
|
{showDiff && (
|
||||||
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import LockBanner from './components/LockBanner.jsx';
|
|||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
|
import ConfirmDialog from './components/ConfirmDialog.jsx';
|
||||||
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
@@ -219,6 +220,7 @@ function IPRangesManager() {
|
|||||||
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
|
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
|
||||||
const [historyOpen, setHistoryOpen] = useState(false);
|
const [historyOpen, setHistoryOpen] = useState(false);
|
||||||
const [wsOpen, setWsOpen] = useState(false);
|
const [wsOpen, setWsOpen] = useState(false);
|
||||||
|
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
|
||||||
|
|
||||||
const handlePreviewDiff = () => {
|
const handlePreviewDiff = () => {
|
||||||
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||||
@@ -421,6 +423,8 @@ function IPRangesManager() {
|
|||||||
disableExport={items.length === 0}
|
disableExport={items.length === 0}
|
||||||
onClear={clearInvalid}
|
onClear={clearInvalid}
|
||||||
disableClear={items.length === 0}
|
disableClear={items.length === 0}
|
||||||
|
onClearCommunities={() => setClearCommunitiesOpen(true)}
|
||||||
|
disableClearCommunities={items.length === 0}
|
||||||
onSave={handleSaveChanges}
|
onSave={handleSaveChanges}
|
||||||
disableSave={loading}
|
disableSave={loading}
|
||||||
onHistory={() => setHistoryOpen(true)}
|
onHistory={() => setHistoryOpen(true)}
|
||||||
@@ -709,6 +713,15 @@ function IPRangesManager() {
|
|||||||
url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'}
|
url={'ws://77.232.38.173:8080/ws/update_bgp?api_key=denozord2502'}
|
||||||
onClose={() => setWsOpen(false)}
|
onClose={() => setWsOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={clearCommunitiesOpen}
|
||||||
|
title={'Очистить все community?'}
|
||||||
|
message={'Поле community будет очищено у всех IP-диапазонов. Это действие не сохраняет изменения автоматически.'}
|
||||||
|
confirmText={'Очистить'}
|
||||||
|
cancelText={'Отмена'}
|
||||||
|
onConfirm={() => { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }}
|
||||||
|
onCancel={() => setClearCommunitiesOpen(false)}
|
||||||
|
/>
|
||||||
{/* Diff Modal */}
|
{/* Diff Modal */}
|
||||||
{showDiff && (
|
{showDiff && (
|
||||||
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
IconDownload,
|
IconDownload,
|
||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
IconPlayerPlay,
|
IconPlayerPlay,
|
||||||
IconBolt
|
IconBolt,
|
||||||
|
IconEraser
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import PortalDropdown from './PortalDropdown.jsx';
|
import PortalDropdown from './PortalDropdown.jsx';
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ function PageHeaderActions({
|
|||||||
disableExport = false,
|
disableExport = false,
|
||||||
onClear,
|
onClear,
|
||||||
disableClear = false,
|
disableClear = false,
|
||||||
|
onClearCommunities,
|
||||||
|
disableClearCommunities = false,
|
||||||
onSave,
|
onSave,
|
||||||
disableSave = false,
|
disableSave = false,
|
||||||
onHistory,
|
onHistory,
|
||||||
@@ -90,6 +93,11 @@ function PageHeaderActions({
|
|||||||
Очистить
|
Очистить
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{onClearCommunities && (
|
||||||
|
<button className="btn btn-outline-secondary" type="button" onClick={onClearCommunities} disabled={disableClearCommunities} title="Сбросить community у всех записей">
|
||||||
|
<IconEraser className="me-1" /> Очистить community
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{onSave && (
|
{onSave && (
|
||||||
<button className="btn btn-primary" type="button" onClick={onSave} disabled={disableSave} title="Сохранить">
|
<button className="btn btn-primary" type="button" onClick={onSave} disabled={disableSave} title="Сохранить">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user