feat: Обновить логику сохранения данных в менеджерах ASNs, Domains и IPRanges, добавив загрузку полного списка и совместимость с изменениями текущей страницы, а также улучшить обработку экспорта данных с уведомлениями об ошибках
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
This commit is contained in:
@@ -253,16 +253,30 @@ function ASNsNewManager() {
|
|||||||
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
|
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
|
||||||
.map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() }));
|
.map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() }));
|
||||||
const unique = deduplicate(valid);
|
const unique = deduplicate(valid);
|
||||||
const payload = { domains: unique.map(i => ({ domain: i.asn, type: i.community })), etag };
|
|
||||||
|
// Загрузим полный список, совместим и сохраним
|
||||||
|
const fullRes = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||||
|
const fullPayload = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
|
||||||
|
const full = fullPayload.map(item => ({ asn: String(item.domain).trim(), community: String(item.type || '').trim() }));
|
||||||
|
const fullMap = new Map(full.map(i => [i.asn, { asn: i.asn, community: i.community }]));
|
||||||
|
const originalPageMap = new Map(originalItems.map(i => [String(i.asn).trim(), true]));
|
||||||
|
const uniqueMap = new Map(unique.map(i => [String(i.asn).trim(), { asn: i.asn, community: i.community }]));
|
||||||
|
for (const key of originalPageMap.keys()) {
|
||||||
|
if (!uniqueMap.has(key)) fullMap.delete(key);
|
||||||
|
}
|
||||||
|
for (const [key, val] of uniqueMap.entries()) fullMap.set(key, val);
|
||||||
|
const fullToSave = Array.from(fullMap.values()).map(i => ({ domain: i.asn, type: i.community }));
|
||||||
|
|
||||||
|
const et = fullRes?.headers?.etag || etag;
|
||||||
|
const payload = { domains: fullToSave, etag: et };
|
||||||
const response = await api.post(`/asns`, payload, { validateStatus: () => true });
|
const response = await api.post(`/asns`, payload, { validateStatus: () => true });
|
||||||
if (response.status === 412) {
|
if (response.status === 412) {
|
||||||
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (response.status >= 400) throw new Error(`Save failed with status ${response.status}`);
|
if (response.status >= 400) throw new Error(`Save failed with status ${response.status}`);
|
||||||
setEtag(response.headers?.etag || etag);
|
setEtag(response.headers?.etag || et);
|
||||||
setOriginalItems(unique);
|
await fetchItems();
|
||||||
setItems(unique);
|
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
notifyMutationSuccess('Изменения сохранены');
|
notifyMutationSuccess('Изменения сохранены');
|
||||||
@@ -293,18 +307,26 @@ function ASNsNewManager() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = async () => {
|
||||||
const header = ['asn', 'community'];
|
try {
|
||||||
const csv = [header, ...items.map(i => [i.asn, i.community])]
|
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : '');
|
||||||
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
const response = await api.get(`/asns`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
|
||||||
.join('\n');
|
const payload = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const all = payload.map(item => ({ asn: item.domain, community: item.type }));
|
||||||
const url = URL.createObjectURL(blob);
|
const header = ['asn', 'community'];
|
||||||
const a = document.createElement('a');
|
const csv = [header, ...all.map(i => [i.asn, i.community])]
|
||||||
a.href = url;
|
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
||||||
a.download = `asns_${new Date().toISOString().split('T')[0]}.csv`;
|
.join('\n');
|
||||||
a.click();
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
URL.revokeObjectURL(url);
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `asns_${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
window.notify?.error('Экспорт ASN не удался', String(e?.message || e));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag&Drop импорт
|
// Drag&Drop импорт
|
||||||
|
|||||||
@@ -256,7 +256,23 @@ function DomainsNewManager() {
|
|||||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
const valid = items.filter(i => 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 response = await api.post(`/domains-new`, { domains: unique, etag }, { validateStatus: () => true });
|
|
||||||
|
// Подгружаем весь список, применяем изменения текущей страницы и сохраняем полные данные
|
||||||
|
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 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() }]));
|
||||||
|
// Удаления: всё, что было на странице, но отсутствует в изменённом наборе
|
||||||
|
for (const key of originalPageMap.keys()) {
|
||||||
|
if (!uniqueMap.has(key)) fullMap.delete(key);
|
||||||
|
}
|
||||||
|
// Добавления/изменения
|
||||||
|
for (const [key, val] of uniqueMap.entries()) fullMap.set(key, val);
|
||||||
|
const fullToSave = Array.from(fullMap.values());
|
||||||
|
|
||||||
|
const et = fullRes?.headers?.etag || etag;
|
||||||
|
const response = await api.post(`/domains-new`, { domains: fullToSave, etag: et }, { validateStatus: () => true });
|
||||||
if (response.status === 412) {
|
if (response.status === 412) {
|
||||||
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
||||||
return;
|
return;
|
||||||
@@ -264,9 +280,9 @@ function DomainsNewManager() {
|
|||||||
if (response.status >= 400) {
|
if (response.status >= 400) {
|
||||||
throw new Error(`Save failed with status ${response.status}`);
|
throw new Error(`Save failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
setEtag(response.headers?.etag || etag);
|
setEtag(response.headers?.etag || et);
|
||||||
setOriginalItems(unique);
|
// После сохранения перезагрузим текущую страницу для консистентности
|
||||||
setItems(unique);
|
await fetchItems();
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
notifyMutationSuccess('Изменения сохранены');
|
notifyMutationSuccess('Изменения сохранены');
|
||||||
@@ -320,18 +336,25 @@ function DomainsNewManager() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = async () => {
|
||||||
const header = ['domain', 'community'];
|
try {
|
||||||
const csv = [header, ...items.map(i => [i.domain, i.community])]
|
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : '');
|
||||||
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
const response = await api.get(`/domains-new`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
|
||||||
.join('\n');
|
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const header = ['domain', 'community'];
|
||||||
const url = URL.createObjectURL(blob);
|
const csv = [header, ...all.map(i => [i.domain, i.community])]
|
||||||
const a = document.createElement('a');
|
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
||||||
a.href = url;
|
.join('\n');
|
||||||
a.download = `domains_${new Date().toISOString().split('T')[0]}.csv`;
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
a.click();
|
const url = URL.createObjectURL(blob);
|
||||||
URL.revokeObjectURL(url);
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `domains_${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
window.notify?.error('Экспорт доменов не удался', String(e?.message || e));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearInvalid = () => {
|
const clearInvalid = () => {
|
||||||
|
|||||||
@@ -273,7 +273,21 @@ function IPRangesManager() {
|
|||||||
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
const valid = items.filter(i => 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 response = await api.post(`/ip-ranges`, { ipRanges: unique, etag }, { validateStatus: () => true });
|
|
||||||
|
// Загрузим полный список, совместим с изменениями текущей страницы и сохраним
|
||||||
|
const fullRes = await api.get(`/ip-ranges`, { 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.ipRange).trim(), { ipRange: String(i.ipRange).trim(), community: String(i.community || '').trim() }]));
|
||||||
|
const originalPageMap = new Map(originalItems.map(i => [String(i.ipRange).trim(), true]));
|
||||||
|
const uniqueMap = new Map(unique.map(i => [String(i.ipRange).trim(), { ipRange: String(i.ipRange).trim(), community: String(i.community).trim() }]));
|
||||||
|
for (const key of originalPageMap.keys()) {
|
||||||
|
if (!uniqueMap.has(key)) fullMap.delete(key);
|
||||||
|
}
|
||||||
|
for (const [key, val] of uniqueMap.entries()) fullMap.set(key, val);
|
||||||
|
const fullToSave = Array.from(fullMap.values());
|
||||||
|
|
||||||
|
const et = fullRes?.headers?.etag || etag;
|
||||||
|
const response = await api.post(`/ip-ranges`, { ipRanges: fullToSave, etag: et }, { validateStatus: () => true });
|
||||||
if (response.status === 412) {
|
if (response.status === 412) {
|
||||||
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
||||||
return;
|
return;
|
||||||
@@ -281,9 +295,8 @@ function IPRangesManager() {
|
|||||||
if (response.status >= 400) {
|
if (response.status >= 400) {
|
||||||
throw new Error(`Save failed with status ${response.status}`);
|
throw new Error(`Save failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
setEtag(response.headers?.etag || etag);
|
setEtag(response.headers?.etag || et);
|
||||||
setOriginalItems(unique);
|
await fetchItems();
|
||||||
setItems(unique);
|
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
notifyMutationSuccess('Изменения сохранены');
|
notifyMutationSuccess('Изменения сохранены');
|
||||||
@@ -314,18 +327,25 @@ function IPRangesManager() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = async () => {
|
||||||
const header = ['ipRange', 'community'];
|
try {
|
||||||
const csv = [header, ...items.map(i => [i.ipRange, i.community])]
|
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : '');
|
||||||
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
const response = await api.get(`/ip-ranges`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
|
||||||
.join('\n');
|
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const header = ['ipRange', 'community'];
|
||||||
const url = URL.createObjectURL(blob);
|
const csv = [header, ...all.map(i => [i.ipRange, i.community])]
|
||||||
const a = document.createElement('a');
|
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
||||||
a.href = url;
|
.join('\n');
|
||||||
a.download = `ip_ranges_${new Date().toISOString().split('T')[0]}.csv`;
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
a.click();
|
const url = URL.createObjectURL(blob);
|
||||||
URL.revokeObjectURL(url);
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `ip_ranges_${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
window.notify?.error('Экспорт IP-диапазонов не удался', String(e?.message || e));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag&Drop импорт
|
// Drag&Drop импорт
|
||||||
|
|||||||
Reference in New Issue
Block a user