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 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)}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={clearCommunitiesOpen}
|
||||
title={'Очистить все community?'}
|
||||
message={'Поле community будет очищено у всех записей в таблице. Это действие не сохраняет изменения автоматически.'}
|
||||
confirmText={'Очистить'}
|
||||
cancelText={'Отмена'}
|
||||
onConfirm={() => { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }}
|
||||
onCancel={() => setClearCommunitiesOpen(false)}
|
||||
/>
|
||||
{/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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() {
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<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('name')}>Название {sortField==='name' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
||||
<th>Описание</th>
|
||||
@@ -271,12 +321,17 @@ function CommunitiesManager() {
|
||||
) : paginated.length === 0 ? (
|
||||
<tr><td colSpan={6} className="text-center text-muted py-4">Нет записей</td></tr>
|
||||
) : paginated.map((item) => (
|
||||
<tr key={item.value} className={editingValue === item.value ? 'table-info' : ''}>
|
||||
<tr key={item.value} className={editingValue === item.value ? 'table-info' : ''}>
|
||||
<td>
|
||||
{editingValue === item.value ? (
|
||||
<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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={clearCommunitiesOpen}
|
||||
title={'Очистить все community?'}
|
||||
message={'Поле community будет очищено у всех доменов. Это действие не сохраняет изменения автоматически.'}
|
||||
confirmText={'Очистить'}
|
||||
cancelText={'Отмена'}
|
||||
onConfirm={() => { setItems(prev => prev.map(i => ({ ...i, community: '' }))); setClearCommunitiesOpen(false); }}
|
||||
onCancel={() => setClearCommunitiesOpen(false)}
|
||||
/>
|
||||
{/* Diff Modal */}
|
||||
{showDiff && (
|
||||
<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 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)}
|
||||
/>
|
||||
<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 */}
|
||||
{showDiff && (
|
||||
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
||||
|
||||
@@ -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({
|
||||
Очистить
|
||||
</button>
|
||||
)}
|
||||
{onClearCommunities && (
|
||||
<button className="btn btn-outline-secondary" type="button" onClick={onClearCommunities} disabled={disableClearCommunities} title="Сбросить community у всех записей">
|
||||
<IconEraser className="me-1" /> Очистить community
|
||||
</button>
|
||||
)}
|
||||
{onSave && (
|
||||
<button className="btn btn-primary" type="button" onClick={onSave} disabled={disableSave} title="Сохранить">
|
||||
{loading ? (
|
||||
|
||||
Reference in New Issue
Block a user