refactor: Replace axios with api module in ASNs, Domains, IPRanges managers, and S3MetaBar for consistent API interaction and improved code maintainability
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m5s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m5s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import api from './lib/api.js';
|
||||
import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
|
||||
import {
|
||||
IconPlus,
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IconFileText
|
||||
} from '@tabler/icons-react';
|
||||
import LockBanner from './components/LockBanner.jsx';
|
||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||
|
||||
@@ -51,7 +52,7 @@ function ASNsNewManager() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/communities`);
|
||||
const res = await api.get(`/communities`);
|
||||
setCommunities(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (e) {
|
||||
// тихо игнорируем
|
||||
@@ -81,21 +82,21 @@ function ASNsNewManager() {
|
||||
localStorage.setItem('uiOwner', owner);
|
||||
const resource = 'asns';
|
||||
const acquire = async () => {
|
||||
try { await axios.post(`${API_URL}/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
try { await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
};
|
||||
acquire();
|
||||
const interval = setInterval(acquire, 60_000);
|
||||
fetchItems();
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
axios.delete(`${API_URL}/locks/${resource}`).catch(() => {});
|
||||
api.delete(`/locks/${resource}`).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/asns`, { params: { offset: 0, limit: 0 } });
|
||||
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0 } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||
const mapped = payload.map(item => ({ asn: item.domain, community: item.type }));
|
||||
setItems(mapped);
|
||||
@@ -223,7 +224,7 @@ function ASNsNewManager() {
|
||||
.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 response = await axios.post(`${API_URL}/asns`, payload, { validateStatus: () => true });
|
||||
const response = await api.post(`/asns`, payload, { validateStatus: () => true });
|
||||
if (response.status === 412) {
|
||||
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
||||
return;
|
||||
@@ -548,6 +549,9 @@ function ASNsNewManager() {
|
||||
</div>
|
||||
</div>
|
||||
{/* Мета-информация будет показана внизу карточки */}
|
||||
{loading ? (
|
||||
<TableSkeleton rows={10} cols={3} />
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
@@ -600,6 +604,7 @@ function ASNsNewManager() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{/* Пагинация */}
|
||||
{totalPages > 1 && (
|
||||
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import api from './lib/api.js';
|
||||
import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
|
||||
import {
|
||||
IconPlus,
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IconFileText
|
||||
} from '@tabler/icons-react';
|
||||
import LockBanner from './components/LockBanner.jsx';
|
||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||
|
||||
@@ -51,7 +52,7 @@ function DomainsNewManager() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/communities`);
|
||||
const res = await api.get(`/communities`);
|
||||
setCommunities(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (e) {
|
||||
// тихо игнорируем
|
||||
@@ -86,21 +87,21 @@ function DomainsNewManager() {
|
||||
localStorage.setItem('uiOwner', owner);
|
||||
const resource = 'domains-new';
|
||||
const acquire = async () => {
|
||||
try { await axios.post(`${API_URL}/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
try { await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
};
|
||||
acquire();
|
||||
const interval = setInterval(acquire, 60_000);
|
||||
fetchItems();
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
axios.delete(`${API_URL}/locks/${resource}`).catch(() => {});
|
||||
api.delete(`/locks/${resource}`).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/domains-new`, { params: { offset: 0, limit: 0 } });
|
||||
const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0 } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||
setItems(payload);
|
||||
setOriginalItems(payload);
|
||||
@@ -227,7 +228,7 @@ 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 axios.post(`${API_URL}/domains-new`, { domains: unique, etag }, { validateStatus: () => true });
|
||||
const response = await api.post(`/domains-new`, { domains: unique, etag }, { validateStatus: () => true });
|
||||
if (response.status === 412) {
|
||||
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
||||
return;
|
||||
@@ -556,6 +557,9 @@ function DomainsNewManager() {
|
||||
</div>
|
||||
</div>
|
||||
{/* Мета-информация будет показана внизу карточки */}
|
||||
{loading ? (
|
||||
<TableSkeleton rows={10} cols={3} />
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
@@ -608,6 +612,7 @@ function DomainsNewManager() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{/* Пагинация */}
|
||||
{totalPages > 1 && (
|
||||
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import api from './lib/api.js';
|
||||
import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
|
||||
import {
|
||||
IconPlus,
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IconFileText
|
||||
} from '@tabler/icons-react';
|
||||
import LockBanner from './components/LockBanner.jsx';
|
||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||
|
||||
@@ -51,7 +52,7 @@ function IPRangesManager() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/communities`);
|
||||
const res = await api.get(`/communities`);
|
||||
setCommunities(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (e) {
|
||||
// тихо игнорируем
|
||||
@@ -97,21 +98,21 @@ function IPRangesManager() {
|
||||
localStorage.setItem('uiOwner', owner);
|
||||
const resource = 'ip-ranges';
|
||||
const acquire = async () => {
|
||||
try { await axios.post(`${API_URL}/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
try { await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
};
|
||||
acquire();
|
||||
const interval = setInterval(acquire, 60_000);
|
||||
fetchItems();
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
axios.delete(`${API_URL}/locks/${resource}`).catch(() => {});
|
||||
api.delete(`/locks/${resource}`).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/ip-ranges`, { params: { offset: 0, limit: 0 } });
|
||||
const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0 } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||
setItems(payload);
|
||||
setOriginalItems(payload);
|
||||
@@ -237,7 +238,7 @@ 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 axios.post(`${API_URL}/ip-ranges`, { ipRanges: unique, etag }, { validateStatus: () => true });
|
||||
const response = await api.post(`/ip-ranges`, { ipRanges: unique, etag }, { validateStatus: () => true });
|
||||
if (response.status === 412) {
|
||||
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
|
||||
return;
|
||||
@@ -564,6 +565,9 @@ function IPRangesManager() {
|
||||
</div>
|
||||
</div>
|
||||
{/* Мета-информация будет показана внизу карточки */}
|
||||
{loading ? (
|
||||
<TableSkeleton rows={10} cols={3} />
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
@@ -616,6 +620,7 @@ function IPRangesManager() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{/* Пагинация */}
|
||||
{totalPages > 1 && (
|
||||
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import api from '../lib/api.js';
|
||||
import { IconHash, IconClock, IconFileText, IconRefresh } from '@tabler/icons-react';
|
||||
|
||||
function S3MetaBar({ keys = [], className = '' }) {
|
||||
@@ -9,7 +9,7 @@ function S3MetaBar({ keys = [], className = '' }) {
|
||||
const fetchMeta = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get('/api/s3/last-modified');
|
||||
const res = await api.get('/s3/last-modified');
|
||||
setMeta(res.data || {});
|
||||
} catch {
|
||||
setMeta({});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export default function TableSkeleton({ rows = 10, cols = 3 }) {
|
||||
const Row = () => (
|
||||
<tr>
|
||||
{Array.from({ length: cols }).map((_, i) => (
|
||||
<td key={i}>
|
||||
<div className="placeholder col-8" style={{ height: 14 }}></div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
{Array.from({ length: cols }).map((_, i) => (
|
||||
<th key={i}><div className="placeholder col-6"></div></th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: rows }).map((_, i) => <Row key={i} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Базовый axios-клиент для всего приложения
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
|
||||
// Авто-ретрай для идемпотентных GET: до 2 попыток с экспоненциальной задержкой
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const config = error?.config || {};
|
||||
const isGet = String(config.method || 'get').toLowerCase() === 'get';
|
||||
const status = error?.response?.status;
|
||||
const retriable = !error.response || (status >= 500 && status !== 501);
|
||||
config.__retryCount = config.__retryCount || 0;
|
||||
if (isGet && retriable && config.__retryCount < 2) {
|
||||
config.__retryCount += 1;
|
||||
const delay = 300 * Math.pow(2, config.__retryCount - 1);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
return api(config);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user