refactor(Managers): implement modal editing for community fields in ASNs, Domains, and IPRanges managers to enhance user experience and streamline editing process
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s

This commit is contained in:
2026-02-16 17:55:32 +07:00
parent 57db189375
commit 6efe497e18
3 changed files with 259 additions and 180 deletions
+83 -54
View File
@@ -58,8 +58,9 @@ function ASNsNewManager() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const searchTimer = useRef(null); const searchTimer = useRef(null);
const didInit = useRef(false); const didInit = useRef(false);
const [editingAsn, setEditingAsn] = useState(null);
const [editingValue, setEditingValue] = useState(''); const [editingValue, setEditingValue] = useState('');
const [editModalOpen, setEditModalOpen] = useState(false);
const [itemBeingEdited, setItemBeingEdited] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
@@ -69,7 +70,6 @@ function ASNsNewManager() {
const [sortField, setSortField] = useState('asn'); const [sortField, setSortField] = useState('asn');
const [sortOrder, setSortOrder] = useState('asc'); const [sortOrder, setSortOrder] = useState('asc');
const [filterCommunity, setFilterCommunity] = useState(''); const [filterCommunity, setFilterCommunity] = useState('');
const editInputRef = useRef(null);
const pageSize = 10; const pageSize = 10;
const [asnNameMap, setAsnNameMap] = useState({}); const [asnNameMap, setAsnNameMap] = useState({});
const [selectedItems, setSelectedItems] = useState(new Set()); const [selectedItems, setSelectedItems] = useState(new Set());
@@ -152,24 +152,28 @@ function ASNsNewManager() {
}; };
const handleEdit = (item) => { const handleEdit = (item) => {
setEditingAsn(item.asn); setItemBeingEdited(item);
setEditingValue(item.community); setEditingValue(item.community);
setEditModalOpen(true);
}; };
const handleSaveEdit = (asn) => { const handleSaveEdit = () => {
if (!itemBeingEdited) return;
if (!isValidCommunity(editingValue)) { if (!isValidCommunity(editingValue)) {
setError('Community должен быть числом.'); setError('Community должен быть числом.');
return; return;
} }
const updatedItems = items.map(i => const updatedItems = items.map(i =>
i.asn === asn ? { ...i, community: String(editingValue).trim() } : i i.asn === itemBeingEdited.asn ? { ...i, community: String(editingValue).trim() } : i
); );
setItems(updatedItems); setItems(updatedItems);
setEditingAsn(null); setEditModalOpen(false);
setItemBeingEdited(null);
}; };
const handleCancelEdit = () => { const handleCancelEdit = () => {
setEditingAsn(null); setEditModalOpen(false);
setItemBeingEdited(null);
}; };
const handleDeleteItem = (asnToDelete) => { const handleDeleteItem = (asnToDelete) => {
@@ -477,13 +481,6 @@ function ASNsNewManager() {
} }
}; };
// Улучшенный инлайн-редакт
useEffect(() => {
if (editingAsn && editInputRef.current) {
editInputRef.current.focus();
}
}, [editingAsn]);
// Глобальные хоткеи: Ctrl+S — сохранить изменения // Глобальные хоткеи: Ctrl+S — сохранить изменения
useEffect(() => { useEffect(() => {
const onKey = (e) => { const onKey = (e) => {
@@ -496,9 +493,15 @@ function ASNsNewManager() {
return () => window.removeEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]); }, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
const handleEditKeyDown = (e, asn) => { const handleEditKeyDown = (e) => {
if (e.key === 'Enter') handleSaveEdit(asn); if (e.key === 'Enter') {
if (e.key === 'Escape') handleCancelEdit(); e.preventDefault();
handleSaveEdit();
}
if (e.key === 'Escape') {
e.preventDefault();
handleCancelEdit();
}
}; };
return ( return (
@@ -734,7 +737,7 @@ function ASNsNewManager() {
</thead> </thead>
<tbody> <tbody>
{paginatedItems.map((item, idx) => ( {paginatedItems.map((item, idx) => (
<tr key={item.asn} className={editingAsn === item.asn ? 'table-active' : (selectedItems.has(item.asn) ? 'table-selected' : '')} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}> <tr key={item.asn} className={selectedItems.has(item.asn) ? 'table-selected' : ''} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}>
<td> <td>
<input <input
type="checkbox" type="checkbox"
@@ -770,42 +773,16 @@ function ASNsNewManager() {
<td>{renderCommunityBadge(item.community)}</td> <td>{renderCommunityBadge(item.community)}</td>
<td className="text-end"> <td className="text-end">
<div className="btn-group btn-group-sm" role="group" aria-label={`Действия с ASN ${item.asn}`}> <div className="btn-group btn-group-sm" role="group" aria-label={`Действия с ASN ${item.asn}`}>
{editingAsn === item.asn ? ( <Tooltip content="Редактировать" position="top">
<> <button className="btn btn-icon btn-outline-primary me-1" aria-label="Редактировать" onClick={() => handleEdit(item)} title="Редактировать">
<CommunityAutocompleteInput <IconEdit size={16} />
value={editingValue} </button>
onChange={setEditingValue} </Tooltip>
communities={communities} <Tooltip content="Удалить" position="top">
onKeyDown={(e) => handleEditKeyDown(e, item.asn)} <button className="btn btn-icon btn-outline-danger" aria-label="Удалить" onClick={() => confirmDelete(item)} title="Удалить">
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} <IconTrash size={16} />
style={{ width: '150px' }} </button>
useShortValue={true} </Tooltip>
/>
<Tooltip content="Сохранить (Enter)">
<button className="btn btn-icon btn-success me-1" onClick={() => handleSaveEdit(item.asn)} disabled={!isValidCommunity(editingValue)} aria-label="Сохранить">
<IconCheck size={16} />
</button>
</Tooltip>
<Tooltip content="Отмена (Esc)">
<button className="btn btn-icon btn-secondary" onClick={handleCancelEdit} aria-label="Отмена">
<IconX size={16} />
</button>
</Tooltip>
</>
) : (
<>
<Tooltip content="Редактировать" position="top">
<button className="btn btn-icon btn-outline-primary me-1" aria-label="Редактировать" onClick={() => handleEdit(item)} title="Редактировать">
<IconEdit size={16} />
</button>
</Tooltip>
<Tooltip content="Удалить" position="top">
<button className="btn btn-icon btn-outline-danger" aria-label="Удалить" onClick={() => confirmDelete(item)} title="Удалить">
<IconTrash size={16} />
</button>
</Tooltip>
</>
)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -856,6 +833,58 @@ function ASNsNewManager() {
sampleHeader={['asn','community']} sampleHeader={['asn','community']}
placeholder={'12345 65000:100'} placeholder={'12345 65000:100'}
/> />
{/* Модальное окно редактирования community */}
{editModalOpen && itemBeingEdited && (
<div className="modal modal-blur fade show" style={{ display: 'block' }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered" role="document">
<div className="modal-content">
<button
type="button"
className="btn-close"
onClick={handleCancelEdit}
aria-label="Закрыть"
></button>
<div className="modal-header">
<h3 className="modal-title">Редактировать community</h3>
</div>
<div className="modal-body">
<p className="text-muted mb-2">
ASN: <span className="font-monospace">{itemBeingEdited.asn}</span>
</p>
<div className="mb-3">
<label className="form-label">Community</label>
<CommunityAutocompleteInput
value={editingValue}
onChange={setEditingValue}
communities={communities}
onKeyDown={handleEditKeyDown}
className={`form-control${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
useShortValue={true}
/>
{!isValidCommunity(editingValue) && (
<div className="invalid-feedback d-block">
Введите корректное значение community (число или N:N).
</div>
)}
</div>
</div>
<div className="modal-footer">
<button className="btn" onClick={handleCancelEdit}>
Отмена
</button>
<button
className="btn btn-primary"
onClick={handleSaveEdit}
disabled={!isValidCommunity(editingValue)}
>
<IconCheck className="icon me-1" size={18} />
Сохранить
</button>
</div>
</div>
</div>
</div>
)}
<HistoryModal <HistoryModal
resource="asns" resource="asns"
show={historyOpen} show={historyOpen}
+93 -73
View File
@@ -65,8 +65,9 @@ function DomainsNewManager() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const searchTimer = useRef(null); const searchTimer = useRef(null);
const didInit = useRef(false); const didInit = useRef(false);
const [editingDomain, setEditingDomain] = useState(null);
const [editingValue, setEditingValue] = useState(''); const [editingValue, setEditingValue] = useState('');
const [editModalOpen, setEditModalOpen] = useState(false);
const [itemBeingEdited, setItemBeingEdited] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
@@ -76,7 +77,6 @@ function DomainsNewManager() {
const [sortField, setSortField] = useState('domain'); const [sortField, setSortField] = useState('domain');
const [sortOrder, setSortOrder] = useState('asc'); const [sortOrder, setSortOrder] = useState('asc');
const [filterCommunity, setFilterCommunity] = useState(''); const [filterCommunity, setFilterCommunity] = useState('');
const editInputRef = useRef(null);
const pageSize = 10; const pageSize = 10;
// Bulk operations // Bulk operations
@@ -159,24 +159,28 @@ function DomainsNewManager() {
}; };
const handleEdit = (item) => { const handleEdit = (item) => {
setEditingDomain(item.domain); setItemBeingEdited(item);
setEditingValue(item.community); setEditingValue(item.community);
setEditModalOpen(true);
}; };
const handleSaveEdit = (domainName) => { const handleSaveEdit = () => {
if (!itemBeingEdited) return;
if (!isValidCommunity(editingValue)) { if (!isValidCommunity(editingValue)) {
setError('Community должен быть числом.'); setError('Community должен быть числом.');
return; return;
} }
const updatedItems = items.map(i => const updatedItems = items.map(i =>
i.domain === domainName ? { ...i, community: String(editingValue).trim() } : i i.domain === itemBeingEdited.domain ? { ...i, community: String(editingValue).trim() } : i
); );
setItems(updatedItems); setItems(updatedItems);
setEditingDomain(null); setEditModalOpen(false);
setItemBeingEdited(null);
}; };
const handleCancelEdit = () => { const handleCancelEdit = () => {
setEditingDomain(null); setEditModalOpen(false);
setItemBeingEdited(null);
}; };
const handleDeleteItem = (domainNameToDelete) => { const handleDeleteItem = (domainNameToDelete) => {
@@ -426,13 +430,6 @@ function DomainsNewManager() {
} }
}; };
// Улучшенный инлайн-редакт
useEffect(() => {
if (editingDomain && editInputRef.current) {
editInputRef.current.focus();
}
}, [editingDomain]);
// Глобальные хоткеи: Ctrl+S — сохранить изменения // Глобальные хоткеи: Ctrl+S — сохранить изменения
useEffect(() => { useEffect(() => {
const onKey = (e) => { const onKey = (e) => {
@@ -445,9 +442,15 @@ function DomainsNewManager() {
return () => window.removeEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]); }, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
const handleEditKeyDown = (e, domainName) => { const handleEditKeyDown = (e) => {
if (e.key === 'Enter') handleSaveEdit(domainName); if (e.key === 'Enter') {
if (e.key === 'Escape') handleCancelEdit(); e.preventDefault();
handleSaveEdit();
}
if (e.key === 'Escape') {
e.preventDefault();
handleCancelEdit();
}
}; };
// Bulk operations handlers // Bulk operations handlers
@@ -734,7 +737,7 @@ function DomainsNewManager() {
{paginatedItems.map((item, idx) => ( {paginatedItems.map((item, idx) => (
<tr <tr
key={item.domain} key={item.domain}
className={editingDomain === item.domain ? 'table-active' : (selectedDomains.has(item.domain) ? 'table-selected' : '')} className={selectedDomains.has(item.domain) ? 'table-selected' : ''}
style={{ style={{
animation: `fadeIn 0.3s ease ${idx * 0.02}s both` animation: `fadeIn 0.3s ease ${idx * 0.02}s both`
}} }}
@@ -772,61 +775,26 @@ function DomainsNewManager() {
</td> </td>
<td className="text-end"> <td className="text-end">
<div className="btn-group btn-group-sm" role="group" aria-label={`Действия с доменом ${item.domain}`}> <div className="btn-group btn-group-sm" role="group" aria-label={`Действия с доменом ${item.domain}`}>
{editingDomain === item.domain ? ( <Tooltip content="Редактировать" position="top">
<> <button
<CommunityAutocompleteInput className="btn btn-icon btn-outline-primary"
value={editingValue} aria-label="Редактировать"
onChange={setEditingValue} onClick={() => handleEdit(item)}
communities={communities} title="Редактировать"
onKeyDown={(e) => handleEditKeyDown(e, item.domain)} >
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} <IconEdit size={16} />
style={{ width: '150px' }} </button>
useShortValue={true} </Tooltip>
/> <Tooltip content="Удалить" position="top">
<Tooltip content="Сохранить (Enter)"> <button
<button className="btn btn-icon btn-outline-danger"
className="btn btn-icon btn-success" aria-label="Удалить"
onClick={() => handleSaveEdit(item.domain)} onClick={() => confirmDelete(item)}
disabled={!isValidCommunity(editingValue)} title="Удалить"
aria-label="Сохранить" >
> <IconTrash size={16} />
<IconCheck size={16} /> </button>
</button> </Tooltip>
</Tooltip>
<Tooltip content="Отмена (Esc)">
<button
className="btn btn-icon btn-secondary"
onClick={handleCancelEdit}
aria-label="Отмена"
>
<IconX size={16} />
</button>
</Tooltip>
</>
) : (
<>
<Tooltip content="Редактировать" position="top">
<button
className="btn btn-icon btn-outline-primary"
aria-label="Редактировать"
onClick={() => handleEdit(item)}
title="Редактировать"
>
<IconEdit size={16} />
</button>
</Tooltip>
<Tooltip content="Удалить" position="top">
<button
className="btn btn-icon btn-outline-danger"
aria-label="Удалить"
onClick={() => confirmDelete(item)}
title="Удалить"
>
<IconTrash size={16} />
</button>
</Tooltip>
</>
)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -895,6 +863,58 @@ function DomainsNewManager() {
url={wsUrl} url={wsUrl}
onClose={() => setWsOpen(false)} onClose={() => setWsOpen(false)}
/> />
{/* Модальное окно редактирования community */}
{editModalOpen && itemBeingEdited && (
<div className="modal modal-blur fade show" style={{ display: 'block' }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered" role="document">
<div className="modal-content">
<button
type="button"
className="btn-close"
onClick={handleCancelEdit}
aria-label="Закрыть"
></button>
<div className="modal-header">
<h3 className="modal-title">Редактировать community</h3>
</div>
<div className="modal-body">
<p className="text-muted mb-2">
Домен: <span className="font-monospace">{itemBeingEdited.domain}</span>
</p>
<div className="mb-3">
<label className="form-label">Community</label>
<CommunityAutocompleteInput
value={editingValue}
onChange={setEditingValue}
communities={communities}
onKeyDown={handleEditKeyDown}
className={`form-control${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
useShortValue={true}
/>
{!isValidCommunity(editingValue) && (
<div className="invalid-feedback d-block">
Введите корректное значение community (число или N:N).
</div>
)}
</div>
</div>
<div className="modal-footer">
<button className="btn" onClick={handleCancelEdit}>
Отмена
</button>
<button
className="btn btn-primary"
onClick={handleSaveEdit}
disabled={!isValidCommunity(editingValue)}
>
<IconCheck className="icon me-1" size={18} />
Сохранить
</button>
</div>
</div>
</div>
</div>
)}
<ConfirmModal <ConfirmModal
show={clearCommunitiesOpen} show={clearCommunitiesOpen}
onClose={() => setClearCommunitiesOpen(false)} onClose={() => setClearCommunitiesOpen(false)}
+83 -53
View File
@@ -59,8 +59,9 @@ function IPRangesManager() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const searchTimer = useRef(null); const searchTimer = useRef(null);
const didInit = useRef(false); const didInit = useRef(false);
const [editingIp, setEditingIp] = useState(null);
const [editingValue, setEditingValue] = useState(''); const [editingValue, setEditingValue] = useState('');
const [editModalOpen, setEditModalOpen] = useState(false);
const [itemBeingEdited, setItemBeingEdited] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
@@ -70,7 +71,6 @@ function IPRangesManager() {
const [sortField, setSortField] = useState('ipRange'); const [sortField, setSortField] = useState('ipRange');
const [sortOrder, setSortOrder] = useState('asc'); const [sortOrder, setSortOrder] = useState('asc');
const [filterCommunity, setFilterCommunity] = useState(''); const [filterCommunity, setFilterCommunity] = useState('');
const editInputRef = useRef(null);
const pageSize = 10; const pageSize = 10;
const [selectedItems, setSelectedItems] = useState(new Set()); const [selectedItems, setSelectedItems] = useState(new Set());
@@ -162,24 +162,28 @@ function IPRangesManager() {
}; };
const handleEdit = (item) => { const handleEdit = (item) => {
setEditingIp(item.ipRange); setItemBeingEdited(item);
setEditingValue(item.community); setEditingValue(item.community);
setEditModalOpen(true);
}; };
const handleSaveEdit = (ipRange) => { const handleSaveEdit = () => {
if (!itemBeingEdited) return;
if (!isValidCommunity(editingValue)) { if (!isValidCommunity(editingValue)) {
setError('Community должен быть числом.'); setError('Community должен быть числом.');
return; return;
} }
const updatedItems = items.map(i => const updatedItems = items.map(i =>
i.ipRange === ipRange ? { ...i, community: String(editingValue).trim() } : i i.ipRange === itemBeingEdited.ipRange ? { ...i, community: String(editingValue).trim() } : i
); );
setItems(updatedItems); setItems(updatedItems);
setEditingIp(null); setEditModalOpen(false);
setItemBeingEdited(null);
}; };
const handleCancelEdit = () => { const handleCancelEdit = () => {
setEditingIp(null); setEditModalOpen(false);
setItemBeingEdited(null);
}; };
const handleDeleteItem = (ipRangeToDelete) => { const handleDeleteItem = (ipRangeToDelete) => {
@@ -499,12 +503,6 @@ function IPRangesManager() {
} }
}; };
// Улучшенный инлайн-редакт
useEffect(() => {
if (editingIp && editInputRef.current) {
editInputRef.current.focus();
}
}, [editingIp]);
// Глобальные хоткеи: Ctrl+S — сохранить изменения // Глобальные хоткеи: Ctrl+S — сохранить изменения
useEffect(() => { useEffect(() => {
const onKey = (e) => { const onKey = (e) => {
@@ -517,9 +515,15 @@ function IPRangesManager() {
return () => window.removeEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]); }, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
const handleEditKeyDown = (e, ipRange) => { const handleEditKeyDown = (e) => {
if (e.key === 'Enter') handleSaveEdit(ipRange); if (e.key === 'Enter') {
if (e.key === 'Escape') handleCancelEdit(); e.preventDefault();
handleSaveEdit();
}
if (e.key === 'Escape') {
e.preventDefault();
handleCancelEdit();
}
}; };
// ===== Аналитика: утилиты ===== // ===== Аналитика: утилиты =====
@@ -977,7 +981,7 @@ function IPRangesManager() {
</thead> </thead>
<tbody> <tbody>
{paginatedItems.map((item, idx) => ( {paginatedItems.map((item, idx) => (
<tr key={item.ipRange} className={editingIp === item.ipRange ? 'table-active' : (selectedItems.has(item.ipRange) ? 'table-selected' : '')} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}> <tr key={item.ipRange} className={selectedItems.has(item.ipRange) ? 'table-selected' : ''} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}>
<td> <td>
<input <input
type="checkbox" type="checkbox"
@@ -1006,42 +1010,16 @@ function IPRangesManager() {
<td>{renderCommunityBadge(item.community)}</td> <td>{renderCommunityBadge(item.community)}</td>
<td className="text-end"> <td className="text-end">
<div className="btn-group btn-group-sm" role="group" aria-label={`Действия с диапазоном ${item.ipRange}`}> <div className="btn-group btn-group-sm" role="group" aria-label={`Действия с диапазоном ${item.ipRange}`}>
{editingIp === item.ipRange ? ( <Tooltip content="Редактировать" position="top">
<> <button className="btn btn-icon btn-outline-primary" aria-label="Редактировать" onClick={() => handleEdit(item)} title="Редактировать">
<CommunityAutocompleteInput <IconEdit size={16} />
value={editingValue} </button>
onChange={setEditingValue} </Tooltip>
communities={communities} <Tooltip content="Удалить" position="top">
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} <button className="btn btn-icon btn-outline-danger" aria-label="Удалить" onClick={() => confirmDelete(item)} title="Удалить">
style={{ width: '150px' }} <IconTrash size={16} />
onKeyDown={(e) => handleEditKeyDown(e, item.ipRange)} </button>
useShortValue={true} </Tooltip>
/>
<Tooltip content="Сохранить (Enter)">
<button className="btn btn-icon btn-success" onClick={() => handleSaveEdit(item.ipRange)} disabled={!isValidCommunity(editingValue)} aria-label="Сохранить">
<IconCheck size={16} />
</button>
</Tooltip>
<Tooltip content="Отмена (Esc)">
<button className="btn btn-icon btn-secondary" onClick={handleCancelEdit} aria-label="Отмена">
<IconX size={16} />
</button>
</Tooltip>
</>
) : (
<>
<Tooltip content="Редактировать" position="top">
<button className="btn btn-icon btn-outline-primary" aria-label="Редактировать" onClick={() => handleEdit(item)} title="Редактировать">
<IconEdit size={16} />
</button>
</Tooltip>
<Tooltip content="Удалить" position="top">
<button className="btn btn-icon btn-outline-danger" aria-label="Удалить" onClick={() => confirmDelete(item)} title="Удалить">
<IconTrash size={16} />
</button>
</Tooltip>
</>
)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -1092,6 +1070,58 @@ function IPRangesManager() {
sampleHeader={['ipRange','community']} sampleHeader={['ipRange','community']}
placeholder={'192.168.1.0/24 65000:100'} placeholder={'192.168.1.0/24 65000:100'}
/> />
{/* Модальное окно редактирования community */}
{editModalOpen && itemBeingEdited && (
<div className="modal modal-blur fade show" style={{ display: 'block' }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered" role="document">
<div className="modal-content">
<button
type="button"
className="btn-close"
onClick={handleCancelEdit}
aria-label="Закрыть"
></button>
<div className="modal-header">
<h3 className="modal-title">Редактировать community</h3>
</div>
<div className="modal-body">
<p className="text-muted mb-2">
IP-диапазон: <span className="font-monospace">{itemBeingEdited.ipRange}</span>
</p>
<div className="mb-3">
<label className="form-label">Community</label>
<CommunityAutocompleteInput
value={editingValue}
onChange={setEditingValue}
communities={communities}
onKeyDown={handleEditKeyDown}
className={`form-control${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
useShortValue={true}
/>
{!isValidCommunity(editingValue) && (
<div className="invalid-feedback d-block">
Введите корректное значение community (число или N:N).
</div>
)}
</div>
</div>
<div className="modal-footer">
<button className="btn" onClick={handleCancelEdit}>
Отмена
</button>
<button
className="btn btn-primary"
onClick={handleSaveEdit}
disabled={!isValidCommunity(editingValue)}
>
<IconCheck className="icon me-1" size={18} />
Сохранить
</button>
</div>
</div>
</div>
</div>
)}
<HistoryModal <HistoryModal
resource="ip-ranges" resource="ip-ranges"
show={historyOpen} show={historyOpen}