fix: Обновление фильтрации и валидации в компонентах ASNsNewManager, AutoUrlManager, DomainsNewManager, IPRangesManager и FilterManager. Добавлена проверка на null для предотвращения ошибок и улучшения стабильности приложения.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s

This commit is contained in:
2025-11-06 23:44:18 +07:00
parent 3a41cc394c
commit 216e05df9f
6 changed files with 63 additions and 47 deletions
+4 -3
View File
@@ -365,9 +365,10 @@ function ASNsNewManager() {
// Локальная фильтрация и пагинация
const filtered = sortedItems.filter(i => {
if (!i) return false;
const byCommunity = !filterCommunity || i.community === filterCommunity;
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;
});
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
@@ -375,7 +376,7 @@ function ASNsNewManager() {
// Подгружаем имена ASN из кэша/внешних API (после вычисления paginatedItems)
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) => {
const cached = getAsnNameSync(asn);
if (cached !== null && typeof cached !== 'undefined') {
@@ -388,7 +389,7 @@ function ASNsNewManager() {
}, [JSON.stringify(paginatedItems)]);
// Для фильтра - список всех уникальных 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) => {
+2 -2
View File
@@ -83,7 +83,7 @@ function AutoUrlManager() {
// Validate 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() }));
if (validUrls.length === 0) {
setMessage('Добавьте хотя бы одну корректную запись (валидный URL и числовой community)');
@@ -302,7 +302,7 @@ function AutoUrlManager() {
<div>
<button
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}
title="Удалить пустые строки"
>
+12 -11
View File
@@ -218,7 +218,7 @@ function DomainsNewManager() {
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
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() }));
const unique = deduplicate(valid);
setDiff(computeDiff(originalItems, unique));
@@ -227,7 +227,7 @@ function DomainsNewManager() {
const handleSaveChanges = async () => {
// подготовим 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() }));
const unique = deduplicate(valid);
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 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 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 [d, c] = l.split(/\s+/);
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 => {
const merged = [...prev, ...parsed];
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 all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
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(','))
.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
@@ -338,7 +338,7 @@ function DomainsNewManager() {
};
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))
);
};
@@ -376,16 +376,17 @@ function DomainsNewManager() {
// Локальная фильтрация и пагинация
const filtered = sortedItems.filter(i => {
if (!i) return false;
const byCommunity = !filterCommunity || i.community === filterCommunity;
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;
});
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
// Для фильтра - список всех уникальных 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) => {
@@ -450,8 +451,8 @@ function DomainsNewManager() {
const handleBulkExport = () => {
if (selectedDomains.size === 0) return;
const selectedItems = items.filter(i => selectedDomains.has(i.domain));
const text = selectedItems.map(i => `${i.domain} ${i.community}`).join('\n');
const selectedItems = items.filter(i => i != null && selectedDomains.has(i.domain));
const text = selectedItems.map(i => `${i.domain || ''} ${i.community || ''}`).join('\n');
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -895,7 +896,7 @@ function DomainsNewManager() {
show={clearCommunitiesOpen}
onClose={() => setClearCommunitiesOpen(false)}
onConfirm={() => {
setItems(prev => prev.filter(i => i.community !== filterCommunity));
setItems(prev => prev.filter(i => i != null && i.community !== filterCommunity));
setClearCommunitiesOpen(false);
}}
title="Удалить записи community?"
+31 -20
View File
@@ -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(() => {
fetchServers();
@@ -460,8 +463,13 @@ function FilterManager() {
}
};
const handleDeleteFilter = async (filter) => {
const updatedFilters = serverFilters.filter(f => f.community !== filter.community);
const handleDeleteFilter = async () => {
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);
setDeleteFilterModalOpen(false);
setFilterToDelete(null);
@@ -617,13 +625,15 @@ function FilterManager() {
const search = filterSearch.trim().toLowerCase();
const filtered = search
? serverFilters.filter((f) => {
if (!f) return false;
const c = String(f.community || '').toLowerCase();
const g = String(f.gateway || '').toLowerCase();
const d = String(f.description || '').toLowerCase();
return c.includes(search) || g.includes(search) || d.includes(search);
})
: serverFilters.slice();
: serverFilters.filter(f => f != null).slice();
const sorted = filtered.sort((a, b) => {
if (!a || !b) return 0;
const av = String(a[sortBy] || '').toLowerCase();
const bv = String(b[sortBy] || '').toLowerCase();
if (av < bv) return sortDir === 'asc' ? -1 : 1;
@@ -645,6 +655,7 @@ function FilterManager() {
// Убрано сохранение режима в localStorage по просьбе пользователя
const startInlineEdit = (filter) => {
if (!filter) return;
setEditingKey(filterKey(filter));
setEditingDraft({
community: filter.community || '',
@@ -1240,12 +1251,12 @@ function FilterManager() {
</tr>
</thead>
<tbody>
{processedFilters.map((filter, index) => {
{processedFilters.filter(f => f != null).map((filter, index) => {
const key = filterKey(filter);
const isEditing = editingKey === key;
const isSelected = selectedFilterKeys.has(key);
return (
<tr key={`${filter.community}-${index}`} className={isEditing ? 'table-info' : ''}>
<tr key={`${filter?.community || ''}-${index}`} className={isEditing ? 'table-info' : ''}>
<td>
<input
className="form-check-input m-0 align-middle"
@@ -1268,7 +1279,7 @@ function FilterManager() {
<span className="badge bg-blue-lt text-blue me-2">
<IconHash size={12} />
</span>
<code className="text-blue">{filter.community}</code>
<code className="text-blue">{filter?.community || ''}</code>
</div>
)}
</td>
@@ -1284,7 +1295,7 @@ function FilterManager() {
<span className="badge bg-green-lt text-green me-2">
<IconServer size={12} />
</span>
<code className="text-green">{filter.gateway}</code>
<code className="text-green">{filter?.gateway || ''}</code>
</div>
)}
</td>
@@ -1296,8 +1307,8 @@ function FilterManager() {
onChange={(e) => setEditingDraft({ ...editingDraft, description: e.target.value })}
/>
) : (
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter.description || ''}>
{filter.description ? (
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter?.description || ''}>
{filter?.description ? (
<span className="text-muted">{filter.description}</span>
) : (
<span className="text-muted fst-italic">Без описания</span>
@@ -1712,26 +1723,26 @@ function FilterManager() {
) : simpleFilters.length === 0 ? (
<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}>
<td>
<input
className="form-control form-control-sm"
value={filter.community}
value={filter?.community || ''}
onChange={e => handleSimpleEditFilter(idx, 'community', e.target.value)}
/>
</td>
<td>
<input
className="form-control form-control-sm"
value={filter.gateway}
value={filter?.gateway || ''}
onChange={e => handleSimpleEditFilter(idx, 'gateway', e.target.value)}
/>
</td>
<td>
<input
className="form-control form-control-sm"
value={filter.description || ''}
value={filter?.description || ''}
onChange={e => handleSimpleEditFilter(idx, 'description', e.target.value)}
/>
</td>
@@ -2041,7 +2052,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
type="text"
className="form-control"
placeholder="65001:200"
value={filter.community}
value={filter?.community || ''}
onChange={(e) => handleChange('community', e.target.value)}
required
/>
@@ -2052,7 +2063,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
type="text"
className="form-control"
placeholder="SWE-HIPHOST"
value={filter.gateway}
value={filter?.gateway || ''}
onChange={(e) => handleChange('gateway', e.target.value)}
required
/>
@@ -2063,7 +2074,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
type="text"
className="form-control"
placeholder="Описание фильтра"
value={filter.description}
value={filter?.description || ''}
onChange={(e) => handleChange('description', e.target.value)}
/>
</div>
@@ -2099,9 +2110,9 @@ function DeleteFilterModal({ show, filter, onDelete, onClose }) {
<div className="modal-body">
<p>Вы уверены, что хотите удалить фильтр?</p>
<div className="alert alert-warning">
<strong>Community:</strong> {filter.community}<br />
<strong>Gateway:</strong> {filter.gateway}<br />
{filter.description && <><strong>Описание:</strong> {filter.description}</>}
<strong>Community:</strong> {filter?.community || ''}<br />
<strong>Gateway:</strong> {filter?.gateway || ''}<br />
{filter?.description && <><strong>Описание:</strong> {filter.description}</>}
</div>
</div>
<div className="modal-footer">
+7 -7
View File
@@ -225,7 +225,7 @@ function IPRangesManager() {
const [analyzeFilters, setAnalyzeFilters] = useState({ community: '', minMask: 0, maxMask: 32, type: 'any', supernet16: false });
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() }));
const unique = deduplicate(valid);
setDiff(computeDiff(originalItems, unique));
@@ -234,7 +234,7 @@ function IPRangesManager() {
const handleSaveChanges = async () => {
// подготовим 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() }));
const unique = deduplicate(valid);
const diffData = computeDiff(originalItems, unique);
@@ -329,7 +329,7 @@ function IPRangesManager() {
const parsed = lines.map(l => {
const [cidr, c] = l.split(/\s+/);
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 => {
const merged = [...prev, ...parsed];
const map = new Map();
@@ -342,7 +342,7 @@ function IPRangesManager() {
};
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))
);
};
@@ -603,7 +603,7 @@ function IPRangesManager() {
const community = String(analyzeFilters.community || '').trim();
if (!community) { window.notify?.error?.('Выберите community'); return; }
// исходные для 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 filtered = allForCommunity.filter(i => {
@@ -641,7 +641,7 @@ function IPRangesManager() {
const applyAnalysisToItems = () => {
if (!analysisPreview) return;
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 = [
...unchangedCidrs.map(ipRange => ({ ipRange, community })),
...afterCidrs.map(ipRange => ({ ipRange, community }))
@@ -1054,7 +1054,7 @@ function IPRangesManager() {
message={`Будут удалены все IP-диапазоны с community "${filterCommunity}". Действие НЕ сохраняет изменения автоматически.`}
confirmText={'Удалить'}
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)}
/>
{/* Analyze Settings Modal */}
+7 -4
View File
@@ -129,7 +129,7 @@ export function useDataManager(config) {
continue;
}
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 });
}
}
@@ -147,6 +147,7 @@ export function useDataManager(config) {
try {
// Валидируем и дедуплицируем
const valid = items.filter(item => {
if (!item) return false;
const itemValid = validateItem ? validateItem(item[itemKey]) : true;
const communityValid = validateCommunity ? validateCommunity(item.community) : true;
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 valB = b[sortField] || '';
if (typeof valA === 'string') valA = valA.toLowerCase();
@@ -210,9 +212,10 @@ export function useDataManager(config) {
// Фильтрация
const filtered = sortedItems.filter(item => {
if (!item) return false;
const byCommunity = !filterCommunity || item.community === filterCommunity;
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;
});
@@ -221,7 +224,7 @@ export function useDataManager(config) {
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize);
// Уникальные 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) => {