refactor(Managers): integrate AddItemModal into ASNs, Domains, and IPRanges managers for improved item addition functionality and UI consistency
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s
This commit is contained in:
@@ -43,6 +43,7 @@ import SavedFilters from './components/SavedFilters.jsx';
|
||||
import BulkActionsBar from './components/BulkActionsBar.jsx';
|
||||
import Tooltip from './components/Tooltip.jsx';
|
||||
import Pagination from './components/Pagination.jsx';
|
||||
import AddItemModal from './components/AddItemModal.jsx';
|
||||
|
||||
const API_URL = '/api';
|
||||
|
||||
@@ -64,6 +65,7 @@ function IPRangesManager() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState(null);
|
||||
const [addModalOpen, setAddModalOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalItems, setTotalItems] = useState(0);
|
||||
const [sortField, setSortField] = useState('ipRange');
|
||||
@@ -139,18 +141,25 @@ function IPRangesManager() {
|
||||
|
||||
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const cidrOk = isValidCidr(newItem.ipRange);
|
||||
const communityOk = isValidCommunity(newItem.community);
|
||||
setNewInvalid({ ipRange: !cidrOk, community: !communityOk });
|
||||
const handleAddItem = (item) => {
|
||||
const ipRange = item?.ipRange || newItem.ipRange;
|
||||
const community = item?.community || newItem.community;
|
||||
|
||||
const cidrOk = isValidCidr(ipRange);
|
||||
const communityOk = isValidCommunity(community);
|
||||
|
||||
if (!cidrOk || !communityOk) {
|
||||
setError('Введите корректный IP-диапазон в формате CIDR (например 192.168.1.0/24) и числовой community.');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setItems([...items, { ipRange: newItem.ipRange.trim(), community: String(newItem.community).trim() }]);
|
||||
setNewItem({ ipRange: '', community: '' });
|
||||
setNewInvalid({ ipRange: false, community: false });
|
||||
const newRange = { ipRange: ipRange.trim(), community: String(community).trim() };
|
||||
setItems([...items, newRange]);
|
||||
if (!item) {
|
||||
setNewItem({ ipRange: '', community: '' });
|
||||
setNewInvalid({ ipRange: false, community: false });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
@@ -783,7 +792,16 @@ function IPRangesManager() {
|
||||
]} />
|
||||
)}
|
||||
actions={(
|
||||
<PageHeaderActions
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setAddModalOpen(true)}
|
||||
title="Добавить новый IP-диапазон"
|
||||
>
|
||||
<IconPlus className="me-1" size={18} />
|
||||
Добавить
|
||||
</button>
|
||||
<PageHeaderActions
|
||||
loading={loading}
|
||||
onPreview={handlePreviewDiff}
|
||||
disablePreview={loading || items.length === 0}
|
||||
@@ -812,124 +830,22 @@ function IPRangesManager() {
|
||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AddItemModal
|
||||
open={addModalOpen}
|
||||
onClose={() => setAddModalOpen(false)}
|
||||
onSubmit={handleAddItem}
|
||||
type="ipRange"
|
||||
communities={communities}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-lg-3">
|
||||
{/* Classic Add New Item Card (improved) */}
|
||||
<div className="card card-md animate-in">
|
||||
<div className="card-header bg-primary-lt">
|
||||
<h3 className="card-title text-primary">
|
||||
<IconPlus className="icon me-2" />
|
||||
Добавить новый IP-диапазон
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleAddItem(); }}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label required">IP-диапазон</label>
|
||||
<div className="input-icon">
|
||||
<span className="input-icon-addon">
|
||||
<IconNetwork size={18} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control${newInvalid.ipRange ? ' is-invalid' : isValidCidr(newItem.ipRange) ? ' is-valid' : ''}`}
|
||||
placeholder="192.168.1.0/24"
|
||||
value={newItem.ipRange}
|
||||
onChange={(e) => setNewItem({ ...newItem, ipRange: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{newInvalid.ipRange && <div className="invalid-feedback">Некорректный CIDR</div>}
|
||||
{isValidCidr(newItem.ipRange) && !newInvalid.ipRange && (<div className="valid-feedback">Корректный CIDR</div>)}
|
||||
<div className="form-text text-muted small">Формат: A.B.C.D/Маска, например 10.0.0.0/24</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label required">Community</label>
|
||||
<div className="input-icon">
|
||||
<span className="input-icon-addon">
|
||||
<IconHash size={18} />
|
||||
</span>
|
||||
<CommunityAutocompleteInput
|
||||
value={newItem.community}
|
||||
onChange={(v) => setNewItem({ ...newItem, community: v })}
|
||||
communities={communities}
|
||||
placeholder="Начните вводить номер или имя"
|
||||
className={`form-control${newInvalid.community ? ' is-invalid' : isValidCommunity(newItem.community) ? ' is-valid' : ''}`}
|
||||
useShortValue={true}
|
||||
/>
|
||||
</div>
|
||||
{newInvalid.community && <div className="invalid-feedback">Неверный формат community</div>}
|
||||
{isValidCommunity(newItem.community) && !newInvalid.community && (<div className="valid-feedback">Корректный формат community</div>)}
|
||||
{(() => {
|
||||
const match = communities.find(c => c.value === String(newItem.community).trim());
|
||||
if (!match) return null;
|
||||
return (
|
||||
<div className="alert alert-info alert-dismissible mt-2 mb-0" role="alert">
|
||||
<div className="d-flex">
|
||||
<div>
|
||||
<strong>{match.name || match.value}</strong>
|
||||
{match.description && <div className="text-muted small">{match.description}</div>}
|
||||
{match.tags && match.tags.length > 0 && (
|
||||
<div className="mt-1">
|
||||
{match.tags.map((tag, idx) => (<span key={idx} className="badge bg-blue-lt text-blue me-1">{tag}</span>))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button type="submit" className="btn btn-primary w-100" disabled={!isValidCidr(newItem.ipRange) || !isValidCommunity(newItem.community)}>
|
||||
<IconPlus className="icon me-2" />
|
||||
Добавить диапазон
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Дополнительные варианты */}
|
||||
<AccordionCard title={'Дополнительные варианты'} defaultOpen={false} className="animate-in" style={{animationDelay: '0.1s'}}>
|
||||
<QuickAddBar
|
||||
placeholder={'192.168.1.0/24 65000:100'}
|
||||
help={'Вставьте строки: CIDR ПРОБЕЛ COMMUNITY'}
|
||||
parseLine={(line) => { const [cidr, c] = String(line).split(/\s+/); return { ipRange: (cidr||'').trim(), community: (c||'').trim() }; }}
|
||||
validateItem={(obj) => isValidCidr(obj.ipRange) && isValidCommunity(obj.community)}
|
||||
onApply={(list) => {
|
||||
setItems(prev => {
|
||||
const merged = [...prev, ...list];
|
||||
const map = new Map();
|
||||
for (const it of merged) {
|
||||
const key = String(it.ipRange).trim();
|
||||
if (!map.has(key)) map.set(key, { ipRange: key, community: String(it.community).trim() });
|
||||
}
|
||||
return Array.from(map.values());
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="card card-md border-dashed mt-3 animate-in"
|
||||
style={{animationDelay: '0.15s'}}
|
||||
onDragOver={(e) => { e.preventDefault(); e.currentTarget.style.borderColor = 'var(--tblr-primary)'; }}
|
||||
onDragLeave={(e) => { e.currentTarget.style.borderColor = ''; }}
|
||||
onDrop={(e) => { e.currentTarget.style.borderColor = ''; onDropImport(e); }}
|
||||
>
|
||||
<div className="card-body text-center text-muted py-4">
|
||||
<IconUpload size={32} className="mb-2 text-muted" />
|
||||
<div className="fw-medium">Перетащите файл TXT/CSV сюда</div>
|
||||
<div className="small">Формат: CIDR community</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionCard>
|
||||
|
||||
|
||||
{/* Карточка действий больше не нужна — действия перенесены в page-header */}
|
||||
</div>
|
||||
|
||||
<div className="col-lg-9">
|
||||
<div className="col-12">
|
||||
{/* BulkActionsBar */}
|
||||
<BulkActionsBar
|
||||
selectedCount={selectedItems.size}
|
||||
|
||||
Reference in New Issue
Block a user