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

This commit is contained in:
2025-08-14 14:21:14 +07:00
parent 7348cc2c79
commit e6878a5b08
5 changed files with 109 additions and 7 deletions
+13
View File
@@ -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 больше не нужен, т.к. используем кастомный автокомплит */}
</>
);
+61 -6
View File
@@ -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>
+13
View File
@@ -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">
+13
View File
@@ -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 ? (