fix: Обновление фильтрации и валидации в компонентах ASNsNewManager, AutoUrlManager, DomainsNewManager, IPRangesManager и FilterManager. Добавлена проверка на null для предотвращения ошибок и улучшения стабильности приложения.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -365,9 +365,10 @@ function ASNsNewManager() {
|
|||||||
|
|
||||||
// Локальная фильтрация и пагинация
|
// Локальная фильтрация и пагинация
|
||||||
const filtered = sortedItems.filter(i => {
|
const filtered = sortedItems.filter(i => {
|
||||||
|
if (!i) return false;
|
||||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||||
const term = String(searchTerm || '').toLowerCase();
|
const term = String(searchTerm || '').toLowerCase();
|
||||||
const bySearch = !term || String(i.asn).toLowerCase().includes(term);
|
const bySearch = !term || String(i.asn || '').toLowerCase().includes(term);
|
||||||
return byCommunity && bySearch;
|
return byCommunity && bySearch;
|
||||||
});
|
});
|
||||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||||
@@ -375,7 +376,7 @@ function ASNsNewManager() {
|
|||||||
|
|
||||||
// Подгружаем имена ASN из кэша/внешних API (после вычисления paginatedItems)
|
// Подгружаем имена ASN из кэша/внешних API (после вычисления paginatedItems)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visible = paginatedItems.map(i => i.asn);
|
const visible = paginatedItems.filter(i => i != null).map(i => i.asn).filter(asn => asn != null);
|
||||||
visible.forEach(async (asn) => {
|
visible.forEach(async (asn) => {
|
||||||
const cached = getAsnNameSync(asn);
|
const cached = getAsnNameSync(asn);
|
||||||
if (cached !== null && typeof cached !== 'undefined') {
|
if (cached !== null && typeof cached !== 'undefined') {
|
||||||
@@ -388,7 +389,7 @@ function ASNsNewManager() {
|
|||||||
}, [JSON.stringify(paginatedItems)]);
|
}, [JSON.stringify(paginatedItems)]);
|
||||||
|
|
||||||
// Для фильтра - список всех уникальных community
|
// Для фильтра - список всех уникальных community
|
||||||
const allCommunities = Array.from(new Set(items.map(i => String(i.community))));
|
const allCommunities = Array.from(new Set(items.filter(i => i != null).map(i => String(i.community || '')).filter(c => c !== '')));
|
||||||
|
|
||||||
// Сортировка по клику
|
// Сортировка по клику
|
||||||
const handleSort = (field) => {
|
const handleSort = (field) => {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function AutoUrlManager() {
|
|||||||
|
|
||||||
// Validate URLs
|
// Validate URLs
|
||||||
const validUrls = urls
|
const validUrls = urls
|
||||||
.filter(u => isValidHttpUrl(u.url) && isValidCommunity(u.community))
|
.filter(u => u != null && isValidHttpUrl(u.url) && isValidCommunity(u.community))
|
||||||
.map(u => ({ url: u.url.trim(), community: String(u.community).trim() }));
|
.map(u => ({ url: u.url.trim(), community: String(u.community).trim() }));
|
||||||
if (validUrls.length === 0) {
|
if (validUrls.length === 0) {
|
||||||
setMessage('Добавьте хотя бы одну корректную запись (валидный URL и числовой community)');
|
setMessage('Добавьте хотя бы одну корректную запись (валидный URL и числовой community)');
|
||||||
@@ -302,7 +302,7 @@ function AutoUrlManager() {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-secondary me-2"
|
className="btn btn-outline-secondary me-2"
|
||||||
onClick={() => setUrls(urls.filter(u => u.url || u.community))}
|
onClick={() => setUrls(urls.filter(u => u != null && (u.url || u.community)))}
|
||||||
disabled={saving || processing || urls.length === 0}
|
disabled={saving || processing || urls.length === 0}
|
||||||
title="Удалить пустые строки"
|
title="Удалить пустые строки"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function DomainsNewManager() {
|
|||||||
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
|
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
|
||||||
|
|
||||||
const handlePreviewDiff = () => {
|
const handlePreviewDiff = () => {
|
||||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
const valid = items.filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||||
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
|
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
|
||||||
const unique = deduplicate(valid);
|
const unique = deduplicate(valid);
|
||||||
setDiff(computeDiff(originalItems, unique));
|
setDiff(computeDiff(originalItems, unique));
|
||||||
@@ -227,7 +227,7 @@ function DomainsNewManager() {
|
|||||||
|
|
||||||
const handleSaveChanges = async () => {
|
const handleSaveChanges = async () => {
|
||||||
// подготовим diff и спросим подтверждение только при массовых изменениях (>10)
|
// подготовим diff и спросим подтверждение только при массовых изменениях (>10)
|
||||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
const valid = items.filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||||
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
|
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
|
||||||
const unique = deduplicate(valid);
|
const unique = deduplicate(valid);
|
||||||
const diffData = computeDiff(originalItems, unique);
|
const diffData = computeDiff(originalItems, unique);
|
||||||
@@ -256,7 +256,7 @@ function DomainsNewManager() {
|
|||||||
// Подгружаем весь список, применяем изменения текущей страницы и сохраняем полные данные
|
// Подгружаем весь список, применяем изменения текущей страницы и сохраняем полные данные
|
||||||
const fullRes = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } });
|
const fullRes = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||||
const full = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
|
const full = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
|
||||||
const fullMap = new Map(full.map(i => [String(i.domain).trim().toLowerCase(), { domain: String(i.domain).trim().toLowerCase(), community: String(i.community || '').trim() }]));
|
const fullMap = new Map(full.filter(i => i != null).map(i => [String(i.domain || '').trim().toLowerCase(), { domain: String(i.domain || '').trim().toLowerCase(), community: String(i.community || '').trim() }]));
|
||||||
const originalPageMap = new Map(originalItems.map(i => [String(i.domain).trim().toLowerCase(), true]));
|
const originalPageMap = new Map(originalItems.map(i => [String(i.domain).trim().toLowerCase(), true]));
|
||||||
const uniqueMap = new Map(unique.map(i => [String(i.domain).trim().toLowerCase(), { domain: String(i.domain).trim().toLowerCase(), community: String(i.community).trim() }]));
|
const uniqueMap = new Map(unique.map(i => [String(i.domain).trim().toLowerCase(), { domain: String(i.domain).trim().toLowerCase(), community: String(i.community).trim() }]));
|
||||||
// Удаления: всё, что было на странице, но отсутствует в изменённом наборе
|
// Удаления: всё, что было на странице, но отсутствует в изменённом наборе
|
||||||
@@ -300,7 +300,7 @@ function DomainsNewManager() {
|
|||||||
const parsed = lines.map(l => {
|
const parsed = lines.map(l => {
|
||||||
const [d, c] = l.split(/\s+/);
|
const [d, c] = l.split(/\s+/);
|
||||||
return { domain: (d || '').toLowerCase(), community: (c || '').trim() };
|
return { domain: (d || '').toLowerCase(), community: (c || '').trim() };
|
||||||
}).filter(i => isValidDomain(i.domain) && isValidCommunity(i.community));
|
}).filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community));
|
||||||
setItems(prev => {
|
setItems(prev => {
|
||||||
const merged = [...prev, ...parsed];
|
const merged = [...prev, ...parsed];
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
@@ -322,7 +322,7 @@ function DomainsNewManager() {
|
|||||||
const response = await api.get(`/domains-new`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
|
const response = await api.get(`/domains-new`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
|
||||||
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||||
const header = ['domain', 'community'];
|
const header = ['domain', 'community'];
|
||||||
const csv = [header, ...all.map(i => [i.domain, i.community])]
|
const csv = [header, ...all.filter(i => i != null).map(i => [i.domain || '', i.community || ''])]
|
||||||
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
||||||
.join('\n');
|
.join('\n');
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
@@ -338,7 +338,7 @@ function DomainsNewManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearInvalid = () => {
|
const clearInvalid = () => {
|
||||||
setItems(prev => prev.filter(i => i.domain || i.community)
|
setItems(prev => prev.filter(i => i != null && (i.domain || i.community))
|
||||||
.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -376,16 +376,17 @@ function DomainsNewManager() {
|
|||||||
|
|
||||||
// Локальная фильтрация и пагинация
|
// Локальная фильтрация и пагинация
|
||||||
const filtered = sortedItems.filter(i => {
|
const filtered = sortedItems.filter(i => {
|
||||||
|
if (!i) return false;
|
||||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||||
const term = String(searchTerm || '').toLowerCase();
|
const term = String(searchTerm || '').toLowerCase();
|
||||||
const bySearch = !term || String(i.domain).toLowerCase().includes(term);
|
const bySearch = !term || String(i.domain || '').toLowerCase().includes(term);
|
||||||
return byCommunity && bySearch;
|
return byCommunity && bySearch;
|
||||||
});
|
});
|
||||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
|
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
|
||||||
|
|
||||||
// Для фильтра - список всех уникальных community
|
// Для фильтра - список всех уникальных community
|
||||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
const allCommunities = Array.from(new Set(items.filter(i => i != null).map(i => i.community).filter(c => c != null)));
|
||||||
|
|
||||||
// Сортировка по клику
|
// Сортировка по клику
|
||||||
const handleSort = (field) => {
|
const handleSort = (field) => {
|
||||||
@@ -450,8 +451,8 @@ function DomainsNewManager() {
|
|||||||
|
|
||||||
const handleBulkExport = () => {
|
const handleBulkExport = () => {
|
||||||
if (selectedDomains.size === 0) return;
|
if (selectedDomains.size === 0) return;
|
||||||
const selectedItems = items.filter(i => selectedDomains.has(i.domain));
|
const selectedItems = items.filter(i => i != null && selectedDomains.has(i.domain));
|
||||||
const text = selectedItems.map(i => `${i.domain} ${i.community}`).join('\n');
|
const text = selectedItems.map(i => `${i.domain || ''} ${i.community || ''}`).join('\n');
|
||||||
const blob = new Blob([text], { type: 'text/plain' });
|
const blob = new Blob([text], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -895,7 +896,7 @@ function DomainsNewManager() {
|
|||||||
show={clearCommunitiesOpen}
|
show={clearCommunitiesOpen}
|
||||||
onClose={() => setClearCommunitiesOpen(false)}
|
onClose={() => setClearCommunitiesOpen(false)}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
setItems(prev => prev.filter(i => i.community !== filterCommunity));
|
setItems(prev => prev.filter(i => i != null && i.community !== filterCommunity));
|
||||||
setClearCommunitiesOpen(false);
|
setClearCommunitiesOpen(false);
|
||||||
}}
|
}}
|
||||||
title="Удалить записи community?"
|
title="Удалить записи community?"
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ function FilterManager() {
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filterKey = (f) => `${String(f.community || '')}||${String(f.gateway || '')}`;
|
const filterKey = (f) => {
|
||||||
|
if (!f) return '';
|
||||||
|
return `${String(f.community || '')}||${String(f.gateway || '')}`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServers();
|
fetchServers();
|
||||||
@@ -460,8 +463,13 @@ function FilterManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteFilter = async (filter) => {
|
const handleDeleteFilter = async () => {
|
||||||
const updatedFilters = serverFilters.filter(f => f.community !== filter.community);
|
if (!filterToDelete || !selectedServer) return;
|
||||||
|
|
||||||
|
const updatedFilters = serverFilters.filter(f => {
|
||||||
|
if (!f || !filterToDelete) return true;
|
||||||
|
return f.community !== filterToDelete.community || f.gateway !== filterToDelete.gateway;
|
||||||
|
});
|
||||||
setServerFilters(updatedFilters);
|
setServerFilters(updatedFilters);
|
||||||
setDeleteFilterModalOpen(false);
|
setDeleteFilterModalOpen(false);
|
||||||
setFilterToDelete(null);
|
setFilterToDelete(null);
|
||||||
@@ -617,13 +625,15 @@ function FilterManager() {
|
|||||||
const search = filterSearch.trim().toLowerCase();
|
const search = filterSearch.trim().toLowerCase();
|
||||||
const filtered = search
|
const filtered = search
|
||||||
? serverFilters.filter((f) => {
|
? serverFilters.filter((f) => {
|
||||||
|
if (!f) return false;
|
||||||
const c = String(f.community || '').toLowerCase();
|
const c = String(f.community || '').toLowerCase();
|
||||||
const g = String(f.gateway || '').toLowerCase();
|
const g = String(f.gateway || '').toLowerCase();
|
||||||
const d = String(f.description || '').toLowerCase();
|
const d = String(f.description || '').toLowerCase();
|
||||||
return c.includes(search) || g.includes(search) || d.includes(search);
|
return c.includes(search) || g.includes(search) || d.includes(search);
|
||||||
})
|
})
|
||||||
: serverFilters.slice();
|
: serverFilters.filter(f => f != null).slice();
|
||||||
const sorted = filtered.sort((a, b) => {
|
const sorted = filtered.sort((a, b) => {
|
||||||
|
if (!a || !b) return 0;
|
||||||
const av = String(a[sortBy] || '').toLowerCase();
|
const av = String(a[sortBy] || '').toLowerCase();
|
||||||
const bv = String(b[sortBy] || '').toLowerCase();
|
const bv = String(b[sortBy] || '').toLowerCase();
|
||||||
if (av < bv) return sortDir === 'asc' ? -1 : 1;
|
if (av < bv) return sortDir === 'asc' ? -1 : 1;
|
||||||
@@ -645,6 +655,7 @@ function FilterManager() {
|
|||||||
// Убрано сохранение режима в localStorage по просьбе пользователя
|
// Убрано сохранение режима в localStorage по просьбе пользователя
|
||||||
|
|
||||||
const startInlineEdit = (filter) => {
|
const startInlineEdit = (filter) => {
|
||||||
|
if (!filter) return;
|
||||||
setEditingKey(filterKey(filter));
|
setEditingKey(filterKey(filter));
|
||||||
setEditingDraft({
|
setEditingDraft({
|
||||||
community: filter.community || '',
|
community: filter.community || '',
|
||||||
@@ -1240,12 +1251,12 @@ function FilterManager() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{processedFilters.map((filter, index) => {
|
{processedFilters.filter(f => f != null).map((filter, index) => {
|
||||||
const key = filterKey(filter);
|
const key = filterKey(filter);
|
||||||
const isEditing = editingKey === key;
|
const isEditing = editingKey === key;
|
||||||
const isSelected = selectedFilterKeys.has(key);
|
const isSelected = selectedFilterKeys.has(key);
|
||||||
return (
|
return (
|
||||||
<tr key={`${filter.community}-${index}`} className={isEditing ? 'table-info' : ''}>
|
<tr key={`${filter?.community || ''}-${index}`} className={isEditing ? 'table-info' : ''}>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-check-input m-0 align-middle"
|
className="form-check-input m-0 align-middle"
|
||||||
@@ -1268,7 +1279,7 @@ function FilterManager() {
|
|||||||
<span className="badge bg-blue-lt text-blue me-2">
|
<span className="badge bg-blue-lt text-blue me-2">
|
||||||
<IconHash size={12} />
|
<IconHash size={12} />
|
||||||
</span>
|
</span>
|
||||||
<code className="text-blue">{filter.community}</code>
|
<code className="text-blue">{filter?.community || ''}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -1284,7 +1295,7 @@ function FilterManager() {
|
|||||||
<span className="badge bg-green-lt text-green me-2">
|
<span className="badge bg-green-lt text-green me-2">
|
||||||
<IconServer size={12} />
|
<IconServer size={12} />
|
||||||
</span>
|
</span>
|
||||||
<code className="text-green">{filter.gateway}</code>
|
<code className="text-green">{filter?.gateway || ''}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -1296,8 +1307,8 @@ function FilterManager() {
|
|||||||
onChange={(e) => setEditingDraft({ ...editingDraft, description: e.target.value })}
|
onChange={(e) => setEditingDraft({ ...editingDraft, description: e.target.value })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter.description || ''}>
|
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter?.description || ''}>
|
||||||
{filter.description ? (
|
{filter?.description ? (
|
||||||
<span className="text-muted">{filter.description}</span>
|
<span className="text-muted">{filter.description}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted fst-italic">Без описания</span>
|
<span className="text-muted fst-italic">Без описания</span>
|
||||||
@@ -1712,26 +1723,26 @@ function FilterManager() {
|
|||||||
) : simpleFilters.length === 0 ? (
|
) : simpleFilters.length === 0 ? (
|
||||||
<tr><td colSpan={4} className="text-center text-muted">Нет фильтров</td></tr>
|
<tr><td colSpan={4} className="text-center text-muted">Нет фильтров</td></tr>
|
||||||
) : (
|
) : (
|
||||||
simpleFilters.map((filter, idx) => (
|
simpleFilters.filter(f => f != null).map((filter, idx) => (
|
||||||
<tr key={idx}>
|
<tr key={idx}>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
value={filter.community}
|
value={filter?.community || ''}
|
||||||
onChange={e => handleSimpleEditFilter(idx, 'community', e.target.value)}
|
onChange={e => handleSimpleEditFilter(idx, 'community', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
value={filter.gateway}
|
value={filter?.gateway || ''}
|
||||||
onChange={e => handleSimpleEditFilter(idx, 'gateway', e.target.value)}
|
onChange={e => handleSimpleEditFilter(idx, 'gateway', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
value={filter.description || ''}
|
value={filter?.description || ''}
|
||||||
onChange={e => handleSimpleEditFilter(idx, 'description', e.target.value)}
|
onChange={e => handleSimpleEditFilter(idx, 'description', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -2041,7 +2052,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="65001:200"
|
placeholder="65001:200"
|
||||||
value={filter.community}
|
value={filter?.community || ''}
|
||||||
onChange={(e) => handleChange('community', e.target.value)}
|
onChange={(e) => handleChange('community', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -2052,7 +2063,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="SWE-HIPHOST"
|
placeholder="SWE-HIPHOST"
|
||||||
value={filter.gateway}
|
value={filter?.gateway || ''}
|
||||||
onChange={(e) => handleChange('gateway', e.target.value)}
|
onChange={(e) => handleChange('gateway', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -2063,7 +2074,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Описание фильтра"
|
placeholder="Описание фильтра"
|
||||||
value={filter.description}
|
value={filter?.description || ''}
|
||||||
onChange={(e) => handleChange('description', e.target.value)}
|
onChange={(e) => handleChange('description', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -2099,9 +2110,9 @@ function DeleteFilterModal({ show, filter, onDelete, onClose }) {
|
|||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<p>Вы уверены, что хотите удалить фильтр?</p>
|
<p>Вы уверены, что хотите удалить фильтр?</p>
|
||||||
<div className="alert alert-warning">
|
<div className="alert alert-warning">
|
||||||
<strong>Community:</strong> {filter.community}<br />
|
<strong>Community:</strong> {filter?.community || ''}<br />
|
||||||
<strong>Gateway:</strong> {filter.gateway}<br />
|
<strong>Gateway:</strong> {filter?.gateway || ''}<br />
|
||||||
{filter.description && <><strong>Описание:</strong> {filter.description}</>}
|
{filter?.description && <><strong>Описание:</strong> {filter.description}</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function IPRangesManager() {
|
|||||||
const [analyzeFilters, setAnalyzeFilters] = useState({ community: '', minMask: 0, maxMask: 32, type: 'any', supernet16: false });
|
const [analyzeFilters, setAnalyzeFilters] = useState({ community: '', minMask: 0, maxMask: 32, type: 'any', supernet16: false });
|
||||||
|
|
||||||
const handlePreviewDiff = () => {
|
const handlePreviewDiff = () => {
|
||||||
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||||
.map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() }));
|
.map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() }));
|
||||||
const unique = deduplicate(valid);
|
const unique = deduplicate(valid);
|
||||||
setDiff(computeDiff(originalItems, unique));
|
setDiff(computeDiff(originalItems, unique));
|
||||||
@@ -234,7 +234,7 @@ function IPRangesManager() {
|
|||||||
|
|
||||||
const handleSaveChanges = async () => {
|
const handleSaveChanges = async () => {
|
||||||
// подготовим diff и спросим подтверждение только при массовых изменениях (>10)
|
// подготовим diff и спросим подтверждение только при массовых изменениях (>10)
|
||||||
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||||
.map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() }));
|
.map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() }));
|
||||||
const unique = deduplicate(valid);
|
const unique = deduplicate(valid);
|
||||||
const diffData = computeDiff(originalItems, unique);
|
const diffData = computeDiff(originalItems, unique);
|
||||||
@@ -329,7 +329,7 @@ function IPRangesManager() {
|
|||||||
const parsed = lines.map(l => {
|
const parsed = lines.map(l => {
|
||||||
const [cidr, c] = l.split(/\s+/);
|
const [cidr, c] = l.split(/\s+/);
|
||||||
return { ipRange: (cidr || '').trim(), community: (c || '').trim() };
|
return { ipRange: (cidr || '').trim(), community: (c || '').trim() };
|
||||||
}).filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community));
|
}).filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community));
|
||||||
setItems(prev => {
|
setItems(prev => {
|
||||||
const merged = [...prev, ...parsed];
|
const merged = [...prev, ...parsed];
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
@@ -342,7 +342,7 @@ function IPRangesManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearInvalid = () => {
|
const clearInvalid = () => {
|
||||||
setItems(prev => prev.filter(i => i.ipRange || i.community)
|
setItems(prev => prev.filter(i => i != null && (i.ipRange || i.community))
|
||||||
.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -603,7 +603,7 @@ function IPRangesManager() {
|
|||||||
const community = String(analyzeFilters.community || '').trim();
|
const community = String(analyzeFilters.community || '').trim();
|
||||||
if (!community) { window.notify?.error?.('Выберите community'); return; }
|
if (!community) { window.notify?.error?.('Выберите community'); return; }
|
||||||
// исходные для community
|
// исходные для community
|
||||||
const allForCommunity = items.filter(i => i.community === community && isValidCidr(i.ipRange));
|
const allForCommunity = items.filter(i => i != null && i.community === community && isValidCidr(i.ipRange));
|
||||||
const totalBefore = allForCommunity.length;
|
const totalBefore = allForCommunity.length;
|
||||||
|
|
||||||
const filtered = allForCommunity.filter(i => {
|
const filtered = allForCommunity.filter(i => {
|
||||||
@@ -641,7 +641,7 @@ function IPRangesManager() {
|
|||||||
const applyAnalysisToItems = () => {
|
const applyAnalysisToItems = () => {
|
||||||
if (!analysisPreview) return;
|
if (!analysisPreview) return;
|
||||||
const { community, afterCidrs, unchangedCidrs } = analysisPreview;
|
const { community, afterCidrs, unchangedCidrs } = analysisPreview;
|
||||||
const keptOthers = items.filter(i => i.community !== community);
|
const keptOthers = items.filter(i => i != null && i.community !== community);
|
||||||
const newCommunityItems = [
|
const newCommunityItems = [
|
||||||
...unchangedCidrs.map(ipRange => ({ ipRange, community })),
|
...unchangedCidrs.map(ipRange => ({ ipRange, community })),
|
||||||
...afterCidrs.map(ipRange => ({ ipRange, community }))
|
...afterCidrs.map(ipRange => ({ ipRange, community }))
|
||||||
@@ -1054,7 +1054,7 @@ function IPRangesManager() {
|
|||||||
message={`Будут удалены все IP-диапазоны с community "${filterCommunity}". Действие НЕ сохраняет изменения автоматически.`}
|
message={`Будут удалены все IP-диапазоны с community "${filterCommunity}". Действие НЕ сохраняет изменения автоматически.`}
|
||||||
confirmText={'Удалить'}
|
confirmText={'Удалить'}
|
||||||
cancelText={'Отмена'}
|
cancelText={'Отмена'}
|
||||||
onConfirm={() => { setItems(prev => prev.filter(i => i.community !== filterCommunity)); setClearCommunitiesOpen(false); }}
|
onConfirm={() => { setItems(prev => prev.filter(i => i != null && i.community !== filterCommunity)); setClearCommunitiesOpen(false); }}
|
||||||
onCancel={() => setClearCommunitiesOpen(false)}
|
onCancel={() => setClearCommunitiesOpen(false)}
|
||||||
/>
|
/>
|
||||||
{/* Analyze Settings Modal */}
|
{/* Analyze Settings Modal */}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export function useDataManager(config) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const prev = mapBefore.get(k);
|
const prev = mapBefore.get(k);
|
||||||
if (String(prev.community) !== String(v.community)) {
|
if (prev && v && String(prev.community || '') !== String(v.community || '')) {
|
||||||
changed.push({ from: prev, to: v });
|
changed.push({ from: prev, to: v });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +147,7 @@ export function useDataManager(config) {
|
|||||||
try {
|
try {
|
||||||
// Валидируем и дедуплицируем
|
// Валидируем и дедуплицируем
|
||||||
const valid = items.filter(item => {
|
const valid = items.filter(item => {
|
||||||
|
if (!item) return false;
|
||||||
const itemValid = validateItem ? validateItem(item[itemKey]) : true;
|
const itemValid = validateItem ? validateItem(item[itemKey]) : true;
|
||||||
const communityValid = validateCommunity ? validateCommunity(item.community) : true;
|
const communityValid = validateCommunity ? validateCommunity(item.community) : true;
|
||||||
return itemValid && communityValid;
|
return itemValid && communityValid;
|
||||||
@@ -198,7 +199,8 @@ export function useDataManager(config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Сортировка
|
// Сортировка
|
||||||
const sortedItems = [...items].sort((a, b) => {
|
const sortedItems = [...items].filter(item => item != null).sort((a, b) => {
|
||||||
|
if (!a || !b) return 0;
|
||||||
let valA = a[sortField] || '';
|
let valA = a[sortField] || '';
|
||||||
let valB = b[sortField] || '';
|
let valB = b[sortField] || '';
|
||||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||||
@@ -210,9 +212,10 @@ export function useDataManager(config) {
|
|||||||
|
|
||||||
// Фильтрация
|
// Фильтрация
|
||||||
const filtered = sortedItems.filter(item => {
|
const filtered = sortedItems.filter(item => {
|
||||||
|
if (!item) return false;
|
||||||
const byCommunity = !filterCommunity || item.community === filterCommunity;
|
const byCommunity = !filterCommunity || item.community === filterCommunity;
|
||||||
const term = String(searchTerm || '').toLowerCase();
|
const term = String(searchTerm || '').toLowerCase();
|
||||||
const bySearch = !term || String(item[itemKey]).toLowerCase().includes(term);
|
const bySearch = !term || String(item[itemKey] || '').toLowerCase().includes(term);
|
||||||
return byCommunity && bySearch;
|
return byCommunity && bySearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,7 +224,7 @@ export function useDataManager(config) {
|
|||||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||||
|
|
||||||
// Уникальные community для фильтра
|
// Уникальные community для фильтра
|
||||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
const allCommunities = Array.from(new Set(items.filter(i => i != null).map(i => i.community).filter(c => c != null)));
|
||||||
|
|
||||||
// Управление сортировкой
|
// Управление сортировкой
|
||||||
const handleSort = (field) => {
|
const handleSort = (field) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user