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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user