From 0f3ad56f871a5a5576f64dfd8bdf830aa86c1bf4 Mon Sep 17 00:00:00 2001 From: shats Date: Wed, 27 Aug 2025 17:25:20 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=B4=D0=B6=D0=B5=D1=80=D0=B0=D1=85=20ASNs,=20Domains=20?= =?UTF-8?q?=D0=B8=20IPRanges,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=B2?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B8=20=D1=81=D0=BE=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B5=D0=B9=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=D1=8B,=20=D0=B0=20=D1=82=D0=B0=D0=BA?= =?UTF-8?q?=D0=B6=D0=B5=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D1=83=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=20=D1=83=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/ASNsNewManager.jsx | 54 ++++++++++++++++++++--------- frontend/src/DomainsNewManager.jsx | 55 +++++++++++++++++++++--------- frontend/src/IPRangesManager.jsx | 52 +++++++++++++++++++--------- 3 files changed, 113 insertions(+), 48 deletions(-) diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index ccf9840..8310ad9 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -253,16 +253,30 @@ function ASNsNewManager() { const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community)) .map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() })); 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 }); if (response.status === 412) { setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.'); return; } if (response.status >= 400) throw new Error(`Save failed with status ${response.status}`); - setEtag(response.headers?.etag || etag); - setOriginalItems(unique); - setItems(unique); + setEtag(response.headers?.etag || et); + await fetchItems(); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); notifyMutationSuccess('Изменения сохранены'); @@ -293,18 +307,26 @@ function ASNsNewManager() { }); }; - const handleExport = () => { - const header = ['asn', 'community']; - const csv = [header, ...items.map(i => [i.asn, 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;' }); - 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); + const handleExport = async () => { + try { + const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); + const response = await api.get(`/asns`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } }); + const payload = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []); + const all = payload.map(item => ({ asn: item.domain, community: item.type })); + const header = ['asn', 'community']; + const csv = [header, ...all.map(i => [i.asn, 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;' }); + 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 импорт diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 6cbbbe9..c1de0bb 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -256,7 +256,23 @@ function DomainsNewManager() { const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community)) .map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() })); 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) { setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.'); return; @@ -264,9 +280,9 @@ function DomainsNewManager() { if (response.status >= 400) { throw new Error(`Save failed with status ${response.status}`); } - setEtag(response.headers?.etag || etag); - setOriginalItems(unique); - setItems(unique); + setEtag(response.headers?.etag || et); + // После сохранения перезагрузим текущую страницу для консистентности + await fetchItems(); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); notifyMutationSuccess('Изменения сохранены'); @@ -320,18 +336,25 @@ function DomainsNewManager() { }); }; - const handleExport = () => { - const header = ['domain', 'community']; - const csv = [header, ...items.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;' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `domains_${new Date().toISOString().split('T')[0]}.csv`; - a.click(); - URL.revokeObjectURL(url); + const handleExport = async () => { + try { + const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); + 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])] + .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + 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 = () => { diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index d8cdb71..92ccd9d 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -273,7 +273,21 @@ function IPRangesManager() { const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community)) .map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() })); 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) { setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.'); return; @@ -281,9 +295,8 @@ function IPRangesManager() { if (response.status >= 400) { throw new Error(`Save failed with status ${response.status}`); } - setEtag(response.headers?.etag || etag); - setOriginalItems(unique); - setItems(unique); + setEtag(response.headers?.etag || et); + await fetchItems(); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); notifyMutationSuccess('Изменения сохранены'); @@ -314,18 +327,25 @@ function IPRangesManager() { }); }; - const handleExport = () => { - const header = ['ipRange', 'community']; - const csv = [header, ...items.map(i => [i.ipRange, 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;' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `ip_ranges_${new Date().toISOString().split('T')[0]}.csv`; - a.click(); - URL.revokeObjectURL(url); + const handleExport = async () => { + try { + const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); + const response = await api.get(`/ip-ranges`, { 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 = ['ipRange', 'community']; + const csv = [header, ...all.map(i => [i.ipRange, 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;' }); + const url = URL.createObjectURL(blob); + 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 импорт