refactor(api): implement abort signal handling in API requests across multiple components to improve cancellation of ongoing requests and prevent memory leaks
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m12s

This commit is contained in:
2026-02-22 23:53:15 +07:00
parent 3fda43cf2c
commit a14f10d048
17 changed files with 278 additions and 128 deletions
+20 -5
View File
@@ -100,16 +100,23 @@ function ASNsNewManager() {
const [wsUrl, setWsUrl] = useState('');
useEffect(() => {
// Load WS URL once
const controller = new AbortController();
(async () => {
try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {}
try {
const r = await api.get('/ws/url', { signal: controller.signal });
setWsUrl(String(r.data?.url || ''));
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
return () => controller.abort();
}, []);
const fetchItems = async () => {
const fetchItems = async (abortSignal) => {
setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } });
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts });
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
const mapped = payload.map(item => ({ asn: String(item.domain), community: String(item.type) }));
const total = mapped.length;
@@ -122,6 +129,7 @@ function ASNsNewManager() {
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
setError('');
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Error fetching ASNs:', error);
setError('Не удалось загрузить список ASN. Проверьте, запущен ли бэкенд.');
} finally {
@@ -129,7 +137,14 @@ function ASNsNewManager() {
}
};
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
useEffect(() => {
if (!didInit.current) {
didInit.current = true;
const controller = new AbortController();
fetchItems(controller.signal);
return () => controller.abort();
}
}, []);
const handleAddItem = (item) => {
const asn = item?.asn || newItem.asn;
+20 -10
View File
@@ -113,9 +113,12 @@ function BillingManager() {
});
useEffect(() => {
fetchBillingData();
fetchExchangeRates();
fetchServers();
const controller = new AbortController();
const signal = controller.signal;
fetchBillingData(signal);
fetchExchangeRates(signal);
fetchServers(signal);
return () => controller.abort();
}, []);
// Автоматическая синхронизация: добавляем серверы без записей в биллинге
@@ -162,11 +165,12 @@ function BillingManager() {
}, [servers, loading]);
const fetchExchangeRates = async () => {
const fetchExchangeRates = async (abortSignal) => {
setRatesLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
// Основной источник: ЦБ РФ (base = RUB)
const cbr = await axios.get('https://www.cbr-xml-daily.ru/latest.js');
const cbr = await axios.get('https://www.cbr-xml-daily.ru/latest.js', opts);
const rubPerUsd = cbr.data && cbr.data.rates?.USD ? 1 / Number(cbr.data.rates.USD) : null;
const rubPerEur = cbr.data && cbr.data.rates?.EUR ? 1 / Number(cbr.data.rates.EUR) : null;
@@ -180,12 +184,13 @@ function BillingManager() {
return;
}
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.warn('ЦБ недоступен, пробуем резервный источник:', error?.message);
}
try {
// Резервный источник: open.er-api
const response = await axios.get('https://open.er-api.com/v6/latest/USD');
const response = await axios.get('https://open.er-api.com/v6/latest/USD', opts);
const rates = response.data?.rates || {};
setExchangeRates({
USD: 1,
@@ -193,6 +198,7 @@ function BillingManager() {
RUB: rates.RUB || 1
});
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Ошибка при загрузке курсов валют:', error);
// Фолбек на разумные значения
setExchangeRates({ USD: 1, EUR: 1.05, RUB: 95 });
@@ -201,19 +207,22 @@ function BillingManager() {
}
};
const fetchServers = async () => {
const fetchServers = async (abortSignal) => {
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const res = await api.get(`/servers`);
const res = await api.get(`/servers`, opts);
setServers(Array.isArray(res.data) ? res.data : []);
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Ошибка при загрузке серверов:', error);
}
};
const fetchBillingData = async () => {
const fetchBillingData = async (abortSignal) => {
setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const response = await api.get(`/billing`);
const response = await api.get(`/billing`, opts);
const rates = exchangeRates; // из closure, на первой загрузке может быть {1,1,1} — для USD достаточно
const toRUB = (amt, ccy) => {
if (!amt) return 0;
@@ -251,6 +260,7 @@ function BillingManager() {
setBillingData(normalized);
setError('');
} catch (err) {
if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return;
console.error('Ошибка при загрузке данных биллинга:', err);
setError('Ошибка при загрузке данных');
setBillingData([]);
+29 -17
View File
@@ -202,18 +202,21 @@ function Dashboard() {
const [expandedPingId, setExpandedPingId] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function fetchStats() {
setLoading(true);
setError(null);
try {
// Загружаем данные с обработкой ошибок для каждого endpoint
const opts = { signal };
const results = await Promise.allSettled([
api.get('/domains-new', { params: { countOnly: true } }),
api.get('/ip-ranges', { params: { countOnly: true } }),
api.get('/asns', { params: { countOnly: true } }),
api.get('/servers'),
api.get('/servers/availability', { params: { ttlSeconds: 60 } }),
api.get('/s3/last-modified')
api.get('/domains-new', { params: { countOnly: true }, ...opts }),
api.get('/ip-ranges', { params: { countOnly: true }, ...opts }),
api.get('/asns', { params: { countOnly: true }, ...opts }),
api.get('/servers', opts),
api.get('/servers/availability', { params: { ttlSeconds: 60 }, ...opts }),
api.get('/s3/last-modified', opts)
]);
// Обрабатываем результаты
@@ -278,6 +281,7 @@ function Dashboard() {
console.warn('Некоторые API недоступны:', errors.map(e => e.reason?.message));
}
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.error('Критическая ошибка загрузки статистики:', e);
setError('Не удалось загрузить статистику');
setStats({
@@ -296,23 +300,26 @@ function Dashboard() {
}
}
fetchStats();
return () => controller.abort();
}, []);
useEffect(() => {
let cancelled = false;
api.get('/ping-services-list')
const controller = new AbortController();
api.get('/ping-services-list', { signal: controller.signal })
.then(({ data }) => {
if (!cancelled && Array.isArray(data?.list)) setPingServicesConfig(data.list);
if (Array.isArray(data?.list)) setPingServicesConfig(data.list);
})
.catch(() => {
if (!cancelled) setPingServicesConfig([]);
.catch((e) => {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setPingServicesConfig([]);
});
return () => { cancelled = true; };
return () => controller.abort();
}, []);
function fetchPingServices(forceRefresh = false) {
function fetchPingServices(forceRefresh = false, abortSignal) {
setPingLoading(true);
api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {} })
const opts = abortSignal ? { signal: abortSignal } : {};
api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {}, ...opts })
.then(({ data }) => {
if (!data || typeof data !== 'object') return;
const byId = data.byId ?? data;
@@ -320,12 +327,17 @@ function Dashboard() {
setPingServices(byId);
setPingHistory(typeof history === 'object' ? history : {});
})
.catch(() => setPingServices(null))
.catch((e) => {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setPingServices(null);
})
.finally(() => setPingLoading(false));
}
useEffect(() => {
fetchPingServices();
const controller = new AbortController();
fetchPingServices(false, controller.signal);
return () => controller.abort();
}, []);
if (error) {
+25 -10
View File
@@ -86,14 +86,25 @@ function DomainsNewManager() {
// Справочник community для подсказок
const [communities, setCommunities] = useState([]);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
(async () => {
try {
const res = await api.get(`/communities`);
const res = await api.get(`/communities`, { signal });
setCommunities(Array.isArray(res.data) ? res.data : []);
} catch (e) {
// тихо игнорируем
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
(async () => {
try {
const r = await api.get('/ws/url', { signal });
setWsUrl(String(r.data?.url || ''));
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
return () => controller.abort();
}, []);
const renderCommunityBadge = (value) => (<CommunityBadge value={value} communities={communities} />);
@@ -110,14 +121,11 @@ function DomainsNewManager() {
const [wsOpen, setWsOpen] = useState(false);
const [wsUrl, setWsUrl] = useState('');
useEffect(() => {
(async () => { try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} })();
}, []);
const fetchItems = async () => {
const fetchItems = async (abortSignal) => {
setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } });
const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts });
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
const total = payload.length;
setItems(payload);
@@ -129,6 +137,7 @@ function DomainsNewManager() {
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
setError('');
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Error fetching domains-new:', error);
setError('Не удалось загрузить домены. Проверьте, запущен ли бэкенд.');
} finally {
@@ -136,8 +145,14 @@ function DomainsNewManager() {
}
};
// Подгрузка при изменении страницы/поиска/фильтра
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
useEffect(() => {
if (!didInit.current) {
didInit.current = true;
const controller = new AbortController();
fetchItems(controller.signal);
return () => controller.abort();
}
}, []);
const handleAddItem = (item) => {
// Если item передан (из модалки), используем его, иначе из state (старый способ)
+21 -11
View File
@@ -57,39 +57,46 @@ function EasySwitchManager() {
const location = useLocation();
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const initData = async () => {
try {
// Загружаем communities
const commRes = await api.get(`/communities`);
const commRes = await api.get(`/communities`, { signal });
const communities = Array.isArray(commRes.data) ? commRes.data : [];
setCommunitiesDirectory(communities);
// Загружаем inventory servers
const invRes = await api.get(`/servers`);
const invRes = await api.get(`/servers`, { signal });
const inventory = Array.isArray(invRes.data) ? invRes.data : [];
setInventoryServers(inventory);
// Загружаем network-config (gateways и туннели)
let ncGateways = [];
try {
const ncRes = await api.get(`/network-config`);
const ncRes = await api.get(`/network-config`, { signal });
const ncData = ncRes.data || {};
ncGateways = Array.isArray(ncData.gateways) ? ncData.gateways : [];
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
// Если файла нет или ошибка - используем пустой массив (fallback на старую логику)
console.warn('EasySwitch: не удалось загрузить /network-config, используется fallback:', e?.message || e);
ncGateways = [];
}
setNetworkConfigGateways(ncGateways);
// Загружаем данные серверов и фильтров
await loadDataWithParams(communities, inventory, ncGateways);
await loadDataWithParams(communities, inventory, ncGateways, signal);
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.error('Error initializing data:', e);
setError('Ошибка загрузки данных');
}
};
initData();
return () => controller.abort();
}, []);
// При переходе на раздел Easy Switch перезапрашиваем пинги (устаревшие по TTL обновятся)
@@ -111,21 +118,23 @@ function EasySwitchManager() {
requestPings(pingTasks);
}, [location.pathname, servers, inventoryServers, requestPings]);
const loadDataWithParams = async (communities, inventory, ncGatewaysParam) => {
const loadDataWithParams = async (communities, inventory, ncGatewaysParam, signal) => {
setLoading(true);
setError('');
const requestConfig = signal ? { signal } : {};
try {
// Загружаем список серверов из server-configs
const serversResponse = await api.get(`/server-configs`);
const serversResponse = await api.get(`/server-configs`, requestConfig);
const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : [];
// Загружаем фильтры для каждого сервера (для определения активных gateways)
const serversWithData = await Promise.all(
serversList.map(async (server) => {
try {
// Загружаем filters для сервера
const filtersResponse = await api.get(`/server-filters/${server.id}`);
const filtersResponse = await api.get(`/server-filters/${server.id}`, requestConfig);
const filters = Array.isArray(filtersResponse.data) ? filtersResponse.data : [];
// Получаем gateways сервера:
@@ -283,6 +292,7 @@ function EasySwitchManager() {
} catch (_) {}
} catch (err) {
if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return;
console.error('Error loading data:', err);
setError('Ошибка загрузки данных. Проверьте подключение к серверу.');
} finally {
+25 -17
View File
@@ -896,47 +896,51 @@ function FilterManager() {
}, [location.pathname, selectedServer, selectedServerGateways, requestPings]);
useEffect(() => {
// Загружаем базовую AS из настроек
const controller = new AbortController();
const signal = controller.signal;
const opts = { signal };
(async () => {
try {
const res = await api.get('/ui-settings');
const res = await api.get('/ui-settings', opts);
const data = res?.data || {};
const loadedBaseAS = String(data?.baseAS || '65001');
setBaseAS(loadedBaseAS);
} catch (e) {
// мягко игнорируем, используем значение по умолчанию
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
(async () => {
try {
const res = await api.get(`/communities`);
const res = await api.get(`/communities`, opts);
setCommunities(Array.isArray(res.data) ? res.data : []);
} catch (e) {
// мягко игнорируем, автокомплит необязателен
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
// Загружаем инвентарь серверов для отображения флагов и IP
(async () => {
try {
const res = await api.get(`/servers`);
const res = await api.get(`/servers`, opts);
setInventoryServers(Array.isArray(res.data) ? res.data : []);
} catch (e) {
// мягко игнорируем, дополнительная информация необязательна
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
// Загружаем gateways из NetworkConfigManager
(async () => {
try {
const res = await api.get(`/network-config`);
const res = await api.get(`/network-config`, opts);
const networkConfig = res.data || {};
setNetworkConfigGateways(Array.isArray(networkConfig.gateways) ? networkConfig.gateways : []);
} catch (e) {
// Если файла нет или ошибка - используем пустой массив (fallback на старую логику)
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setNetworkConfigGateways([]);
}
})();
return () => controller.abort();
}, []);
const filterKey = (f) => {
@@ -945,7 +949,9 @@ function FilterManager() {
};
useEffect(() => {
fetchServers();
const controller = new AbortController();
fetchServers(controller.signal);
return () => controller.abort();
}, []);
// Загружаем фильтры при выборе сервера
@@ -1003,11 +1009,12 @@ function FilterManager() {
// Функции для управления серверами
// Загружаем jumphost'ы из раздела "Серверы" (/api/servers)
// + загружаем старые server-configs для маппинга legacyId
const fetchServers = async () => {
const fetchServers = async (abortSignal) => {
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const [serversRes, legacyRes] = await Promise.all([
api.get(`/servers`),
api.get(`/server-configs`).catch(() => ({ data: [] })) // Старые конфиги для маппинга
api.get(`/servers`, opts),
api.get(`/server-configs`, opts).catch(() => ({ data: [] })) // Старые конфиги для маппинга
]);
const allServers = Array.isArray(serversRes.data) ? serversRes.data : [];
@@ -1064,6 +1071,7 @@ function FilterManager() {
setServers(jumphosts);
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Error fetching servers:', error);
setServers([]);
}
+22 -4
View File
@@ -114,13 +114,23 @@ function IPRangesManager() {
const [wsUrl, setWsUrl] = useState('');
useEffect(() => {
(async () => { try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} })();
const controller = new AbortController();
(async () => {
try {
const r = await api.get('/ws/url', { signal: controller.signal });
setWsUrl(String(r.data?.url || ''));
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
}
})();
return () => controller.abort();
}, []);
const fetchItems = async () => {
const fetchItems = async (abortSignal) => {
setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' } });
const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts });
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
const total = payload.length;
setItems(payload);
@@ -132,6 +142,7 @@ function IPRangesManager() {
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
setError('');
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Error fetching ip-ranges:', error);
setError('Не удалось загрузить IP-диапазоны. Проверьте, запущен ли бэкенд.');
} finally {
@@ -139,7 +150,14 @@ function IPRangesManager() {
}
};
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
useEffect(() => {
if (!didInit.current) {
didInit.current = true;
const controller = new AbortController();
fetchItems(controller.signal);
return () => controller.abort();
}
}, []);
const handleAddItem = (item) => {
const ipRange = item?.ipRange || newItem.ipRange;
+8 -3
View File
@@ -30,13 +30,16 @@ export default function InterfaceSpeedTest() {
});
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const load = async () => {
setLoading(true);
try {
const [serversRes, netRes, uiRes] = await Promise.all([
api.get('/servers'),
api.get('/network-config').catch(() => ({ data: null })),
api.get('/ui-settings').catch(() => ({ data: {} })),
api.get('/servers', { signal }),
api.get('/network-config', { signal }).catch(() => ({ data: null })),
api.get('/ui-settings', { signal }).catch(() => ({ data: {} })),
]);
setServers(Array.isArray(serversRes?.data) ? serversRes.data : []);
@@ -69,6 +72,7 @@ export default function InterfaceSpeedTest() {
cacheMinutes,
});
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.error('[InterfaceSpeedTest] load failed', e);
notify.error('Не удалось загрузить данные для замера скорости');
} finally {
@@ -76,6 +80,7 @@ export default function InterfaceSpeedTest() {
}
};
load();
return () => controller.abort();
}, [notify]);
const gateways = useMemo(
+20 -10
View File
@@ -157,13 +157,18 @@ function MikrotikBackupsManager() {
const backupPageSize = 10;
useEffect(() => {
fetchServers();
fetchUiSettings();
const controller = new AbortController();
const signal = controller.signal;
fetchServers(signal);
fetchUiSettings(signal);
return () => controller.abort();
}, []);
useEffect(() => {
if (selectedServerId) {
fetchBackups(selectedServerId);
const controller = new AbortController();
fetchBackups(selectedServerId, controller.signal);
return () => controller.abort();
} else {
setBackups([]);
setSelectedKeys([]);
@@ -314,10 +319,11 @@ function MikrotikBackupsManager() {
setBackupServerIds(new Set());
};
const fetchServers = async () => {
const fetchServers = async (abortSignal) => {
const opts = abortSignal ? { signal: abortSignal } : {};
try {
setLoadingServers(true);
const res = await api.get('/servers');
const res = await api.get('/servers', opts);
const list = Array.isArray(res.data) ? res.data : [];
setServers(list);
const firstJumphost = list.find((s) => String(s.type || '').toLowerCase() === 'jumphost');
@@ -325,6 +331,7 @@ function MikrotikBackupsManager() {
setSelectedServerId(makeServerId(firstJumphost));
}
} catch (err) {
if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return;
console.error('Error fetching servers for backups:', err);
notify.error('Не удалось загрузить список серверов для бэкапов');
} finally {
@@ -332,9 +339,10 @@ function MikrotikBackupsManager() {
}
};
const fetchUiSettings = async () => {
const fetchUiSettings = async (abortSignal) => {
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const res = await api.get('/ui-settings');
const res = await api.get('/ui-settings', opts);
const data = res?.data || {};
setUiSettings(data);
const list = Array.isArray(data.mikrotikBackupServers) ? data.mikrotikBackupServers : [];
@@ -342,21 +350,23 @@ function MikrotikBackupsManager() {
const e = res?.headers?.etag || res?.headers?.ETag || '';
setUiSettingsEtag(e ? String(e) : '');
} catch (err) {
if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return;
console.error('Error fetching UI settings for backups:', err);
// мягко игнорируем, просто будут дефолты
}
};
const fetchBackups = async (serverId) => {
const fetchBackups = async (serverId, abortSignal) => {
if (!serverId) return;
const opts = abortSignal ? { signal: abortSignal } : {};
try {
setLoadingBackups(true);
setSelectedKeys([]);
setDiffResult(null);
const res = await api.get('/mikrotik/backups', { params: { serverId } });
const res = await api.get('/mikrotik/backups', { params: { serverId }, ...opts });
const items = res.data?.items || [];
setBackups(items);
} catch (err) {
if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return;
console.error('Error fetching backups:', err);
notify.error('Не удалось загрузить список бэкапов MikroTik');
} finally {
+10 -6
View File
@@ -86,16 +86,17 @@ export default function NetworkMapDashboard() {
const cacheAppliedAtRef = useRef(null);
/** Связи строятся из интерфейсов раздела «Сетевые настройки» (/network-config), tunnelInterfaces */
const fetchData = useCallback(async () => {
const fetchData = useCallback(async (abortSignal) => {
setLoading(true);
setError(null);
cacheAppliedAtRef.current = null;
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const [serversRes, configRes, uiSettingsRes, cacheRes] = await Promise.all([
api.get('/servers'),
api.get('/network-config').catch(() => ({ data: null })),
api.get('/ui-settings').catch(() => ({ data: {} })),
api.get('/network-map-cache').catch(() => ({ data: {} })),
api.get('/servers', opts),
api.get('/network-config', opts).catch(() => ({ data: null })),
api.get('/ui-settings', opts).catch(() => ({ data: {} })),
api.get('/network-map-cache', opts).catch(() => ({ data: {} })),
]);
const serversList = Array.isArray(serversRes?.data) ? serversRes.data : [];
const config = configRes?.data || {};
@@ -181,6 +182,7 @@ export default function NetworkMapDashboard() {
setSpeedStaleMap({});
}
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.error('NetworkMap fetch:', e);
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить данные');
setServers([]);
@@ -342,7 +344,9 @@ export default function NetworkMapDashboard() {
}, [connections]);
useEffect(() => {
fetchData();
const controller = new AbortController();
fetchData(controller.signal);
return () => controller.abort();
}, [fetchData]);
// Пинг и скорость не запускаются автоматически при открытии карты (даже если кеш просрочен).
+8 -6
View File
@@ -33,9 +33,10 @@ export default function NetworkMapSchedulerPage() {
const logContainerRef = useRef(null);
const [autoScroll, setAutoScroll] = useState(true);
const fetchSettings = useCallback(async () => {
const fetchSettings = useCallback(async (abortSignal) => {
try {
const { data } = await api.get('/scheduler/network-map/settings');
const opts = abortSignal ? { signal: abortSignal } : {};
const { data } = await api.get('/scheduler/network-map/settings', opts);
setSettings({
enabled: Boolean(data.enabled),
intervalMinutes: Math.max(INTERVAL_MIN, Math.min(INTERVAL_MAX, Number(data.intervalMinutes) || 5)),
@@ -45,6 +46,7 @@ export default function NetworkMapSchedulerPage() {
});
setNextRunAt(typeof data.nextRunAt === 'number' ? data.nextRunAt : null);
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить настройки');
}
}, []);
@@ -59,13 +61,13 @@ export default function NetworkMapSchedulerPage() {
}, []);
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
(async () => {
setLoading(true);
await fetchSettings();
if (!cancelled) setLoading(false);
await fetchSettings(controller.signal);
setLoading(false);
})();
return () => { cancelled = true; };
return () => controller.abort();
}, [fetchSettings]);
useEffect(() => {
+7 -3
View File
@@ -58,11 +58,12 @@ export default function PingServicesManager() {
const [editingItem, setEditingItem] = useState(null);
const [editDraft, setEditDraft] = useState({ id: '', name: '', host: '', port: '443', icon: 'IconBrandGoogle', color: 'blue' });
const loadSettings = async () => {
const loadSettings = async (abortSignal) => {
setLoading(true);
setError('');
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const res = await api.get('/ui-settings');
const res = await api.get('/ui-settings', opts);
const data = res?.data || {};
setRawSettings(data);
setEtag(String(res?.headers?.etag || res?.headers?.ETag || ''));
@@ -76,6 +77,7 @@ export default function PingServicesManager() {
color: typeof s?.color === 'string' ? s.color : 'primary',
})).filter((s) => s.host));
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.error(e);
setError('Не удалось загрузить настройки');
} finally {
@@ -84,7 +86,9 @@ export default function PingServicesManager() {
};
useEffect(() => {
loadSettings();
const controller = new AbortController();
loadSettings(controller.signal);
return () => controller.abort();
}, []);
const saveSettings = async () => {
+7 -3
View File
@@ -146,14 +146,16 @@ export default function ResourceStatsPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchStats = useCallback(async () => {
const fetchStats = useCallback(async (abortSignal) => {
setLoading(true);
setError(null);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const res = await api.get('/resources/stats');
const res = await api.get('/resources/stats', opts);
const data = res?.data;
setRouters(Array.isArray(data?.routers) ? data.routers : []);
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику');
setRouters([]);
} finally {
@@ -162,7 +164,9 @@ export default function ResourceStatsPage() {
}, []);
useEffect(() => {
fetchStats();
const controller = new AbortController();
fetchStats(controller.signal);
return () => controller.abort();
}, [fetchStats]);
return (
+18 -9
View File
@@ -127,21 +127,26 @@ function ServerManager() {
};
// === Загрузка данных ===
const fetchNetworkConfig = async () => {
const fetchNetworkConfig = async (abortSignal) => {
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const res = await api.get('/network-config');
const res = await api.get('/network-config', opts);
const config = res.data || {};
setNetworkConfigGateways(Array.isArray(config.gateways) ? config.gateways : []);
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.warn('Network config not loaded:', e?.message || e);
setNetworkConfigGateways([]);
}
};
useEffect(() => {
fetchServers();
fetchConnections();
fetchNetworkConfig();
const controller = new AbortController();
const signal = controller.signal;
fetchServers(signal);
fetchConnections(signal);
fetchNetworkConfig(signal);
return () => controller.abort();
}, []);
useEffect(() => {
@@ -156,10 +161,11 @@ function ServerManager() {
}
}, []);
const fetchServers = async () => {
const fetchServers = async (abortSignal) => {
setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const response = await api.get(`/servers`);
const response = await api.get(`/servers`, opts);
const normalized = (Array.isArray(response.data) ? response.data : []).map((srv) => ({
...srv,
type: normalizeServerType(srv.type),
@@ -167,6 +173,7 @@ function ServerManager() {
}));
setServers(normalized);
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Error fetching servers:', error);
notify.error('Не удалось загрузить серверы. Проверьте, запущен ли бэкенд.');
} finally {
@@ -174,15 +181,17 @@ function ServerManager() {
}
};
const fetchConnections = async () => {
const fetchConnections = async (abortSignal) => {
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const response = await api.get(`/server-connections`);
const response = await api.get(`/server-connections`, opts);
if (Array.isArray(response.data)) {
setConnections(response.data);
} else {
setConnections([]);
}
} catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
console.error('Error fetching server connections:', error);
}
};
+8 -4
View File
@@ -181,18 +181,20 @@ export default function TrafficDashboard() {
const interfaceFilter = getTrafficInterfaceFilter(uiSettings);
const fetchStats = useCallback(async () => {
const fetchStats = useCallback(async (abortSignal) => {
setLoading(true);
setError(null);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const [statsRes, settingsRes] = await Promise.all([
api.get('/traffic/interface-stats'),
api.get('/ui-settings').catch(() => ({ data: {} })),
api.get('/traffic/interface-stats', opts),
api.get('/ui-settings', opts).catch(() => ({ data: {} })),
]);
const data = statsRes?.data || {};
setJumphosts(Array.isArray(data?.jumphosts) ? data.jumphosts : []);
setUiSettings(settingsRes?.data || {});
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
console.error('Traffic stats:', e);
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику');
setJumphosts([]);
@@ -202,7 +204,9 @@ export default function TrafficDashboard() {
}, []);
useEffect(() => {
fetchStats();
const controller = new AbortController();
fetchStats(controller.signal);
return () => controller.abort();
}, [fetchStats]);
const filteredJumphosts = useMemo(() => {
+20 -6
View File
@@ -103,13 +103,16 @@ export default function UptimeMonitorPage() {
[servers]
);
const fetchServers = useCallback(async () => {
const controllerRef = useRef(null);
const fetchServers = useCallback(async (abortSignal) => {
setLoading(true);
setError(null);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const [serversRes, settingsRes] = await Promise.all([
api.get('/servers'),
api.get('/ui-settings').catch(() => ({ data: {} })),
api.get('/servers', opts),
api.get('/ui-settings', opts).catch(() => ({ data: {} })),
]);
const data = serversRes?.data;
setServers(Array.isArray(data) ? data : []);
@@ -117,6 +120,7 @@ export default function UptimeMonitorPage() {
const interval = Math.max(30, Math.min(86400, parseInt(ui.uptimeMonitorIntervalSeconds, 10) || DEFAULT_CHECK_INTERVAL_SEC));
setCheckIntervalSeconds(interval);
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить серверы');
setServers([]);
} finally {
@@ -125,19 +129,29 @@ export default function UptimeMonitorPage() {
}, []);
useEffect(() => {
fetchServers();
const c = new AbortController();
controllerRef.current = c;
fetchServers(c.signal);
return () => {
c.abort();
controllerRef.current = null;
};
}, [fetchServers]);
// При заходе на страницу подставляем кеш сразу видны последние проверки
useEffect(() => {
if (jumphosts.length === 0) return;
api.get('/uptime/cache')
const signal = controllerRef.current?.signal;
if (!signal) return;
api.get('/uptime/cache', { signal })
.then(({ data }) => {
if (data?.results && typeof data.results === 'object' && Object.keys(data.results).length > 0) {
setStatusMap((prev) => ({ ...prev, ...data.results }));
}
})
.catch(() => {});
.catch((e) => {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
});
}, [jumphosts]);
const checkIntervalMs = checkIntervalSeconds * 1000;
+10 -4
View File
@@ -7,19 +7,25 @@ function S3MetaBar({ keys = [], className = '' }) {
const [meta, setMeta] = useState({});
const [loading, setLoading] = useState(false);
const fetchMeta = async () => {
const fetchMeta = async (abortSignal) => {
setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try {
const res = await api.get('/s3/last-modified');
const res = await api.get('/s3/last-modified', opts);
setMeta(res.data || {});
} catch {
} catch (e) {
if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return;
setMeta({});
} finally {
setLoading(false);
}
};
useEffect(() => { fetchMeta(); }, []);
useEffect(() => {
const controller = new AbortController();
fetchMeta(controller.signal);
return () => controller.abort();
}, []);
const copyMeta = async (k) => {
try {