From 0746fb4f1e61b1041d1ce0174f38d94c3dd4ec54 Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 7 Apr 2026 23:36:36 +0700 Subject: [PATCH] =?UTF-8?q?fix(evobgp):=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D1=8B=20=D0=BA=20API=20=D0=BD=D0=B0=20=D0=B0=D0=B1=D1=81?= =?UTF-8?q?=D0=BE=D0=BB=D1=8E=D1=82=D0=BD=D1=8B=D0=B9=20URL=20EvoBGP,=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BD=D0=B0=20=D1=85=D0=BE=D1=81=D1=82=20SPA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - base URL: VITE_EVOBGP_API_URL или localStorage evobgp_api_url, в dev — proxy /v1 - нормализация до …/v1; убран относительный /v1 в production (запросы больше не идут на порт UI) - форма URL и токена в Communities для сохранения в localStorage Made-with: Cursor --- frontend/src/CommunitiesManager.jsx | 101 +++++++++++++++++++++++++++- frontend/src/lib/evobgpData.js | 59 +++++++++++++++- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index cbdd0c1..bf1774b 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -16,7 +16,8 @@ import { IconSortDescending, IconUpload, IconDownload, - IconDeviceFloppy + IconDeviceFloppy, + IconPlugConnected } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; import PortalDropdown from './components/PortalDropdown.jsx'; @@ -30,7 +31,7 @@ import ErrorAlert from './components/ErrorAlert.jsx'; import Pagination from './components/Pagination.jsx'; import IconPicker from './components/IconPicker.jsx'; import { getIconById } from './lib/brandIcons.js'; -import { listCommunities, saveCommunitiesData } from './lib/evobgpData.js'; +import { listCommunities, saveCommunitiesData, normalizeEvobgpApiBaseUrl } from './lib/evobgpData.js'; const normalizeCommunityValue = (value) => { const trimmed = String(value || '').trim(); @@ -60,7 +61,18 @@ function CommunitiesManager() { const [filterUsageLoading, setFilterUsageLoading] = useState(false); const [filterUsageError, setFilterUsageError] = useState(''); const [showOnlyUsedInFilters, setShowOnlyUsedInFilters] = useState(false); - + + const [evobgpUrlDraft, setEvobgpUrlDraft] = useState(''); + const [evobgpTokenDraft, setEvobgpTokenDraft] = useState(''); + const [evobgpSettingsOpen, setEvobgpSettingsOpen] = useState(() => { + if (import.meta.env?.VITE_EVOBGP_API_URL) return false; + try { + return !window.localStorage?.getItem('evobgp_api_url'); + } catch { + return true; + } + }); + // Модальные окна const [addModalOpen, setAddModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); @@ -72,6 +84,15 @@ function CommunitiesManager() { fetchFilterUsage(); }, []); + useEffect(() => { + try { + setEvobgpUrlDraft(String(window.localStorage?.getItem('evobgp_api_url') || '')); + setEvobgpTokenDraft(String(window.localStorage?.getItem('evobgp_api_token') || '')); + } catch { + /* no-op */ + } + }, []); + useEffect(() => { if (!filterUsage) return; setItems((prev) => { @@ -271,6 +292,26 @@ function CommunitiesManager() { const handleSave = () => saveAll(items); + const saveEvobgpConnection = () => { + const norm = normalizeEvobgpApiBaseUrl(evobgpUrlDraft); + if (!norm) { + setError('Укажите корректный URL EvoBGP (например http://192.168.0.1:8080). Порт должен совпадать с API EvoBGP, не с этим интерфейсом.'); + return; + } + try { + window.localStorage.setItem('evobgp_api_url', norm); + window.localStorage.setItem('evobgp_api_token', String(evobgpTokenDraft || '').trim()); + } catch (_e) { + setError('Не удалось сохранить настройки в localStorage.'); + return; + } + setEvobgpUrlDraft(norm); + setError(''); + setSuccess('Подключение к EvoBGP сохранено. Загружаем данные…'); + fetchItems(); + setTimeout(() => setSuccess(''), 4000); + }; + const sorted = useMemo(() => { const arr = [...items]; arr.sort((a, b) => { @@ -312,6 +353,60 @@ function CommunitiesManager() { icon={} /> +
+
+
+ + Подключение к EvoBGP API +
+ +
+ {evobgpSettingsOpen && ( +
+

+ Укажите прямой URL сервера EvoBGP (схема + хост + порт API), например{' '} + http://192.168.100.67:8080 — не адрес этого веб-интерфейса. К пути будет добавлено{' '} + /v1 автоматически. Токен — Bearer из EvoBGP. +

+
+
+ + setEvobgpUrlDraft(e.target.value)} + /> +
+
+ + setEvobgpTokenDraft(e.target.value)} + /> +
+
+ +
+
+
+ )} +
+ {/* Уведомления */} {error && ( setError('')} onRetry={fetchItems} /> diff --git a/frontend/src/lib/evobgpData.js b/frontend/src/lib/evobgpData.js index 7f4b01b..ab1cc76 100644 --- a/frontend/src/lib/evobgpData.js +++ b/frontend/src/lib/evobgpData.js @@ -3,19 +3,74 @@ import axios from 'axios'; const DEFAULT_TIMEOUT_MS = 30000; const PAGE_LIMIT = 500; +const LS_KEY_URL = 'evobgp_api_url'; +const LS_KEY_TOKEN = 'evobgp_api_token'; + +/** + * Приводит введённый URL к базе API EvoBGP (…/v1), без завершающего слэша. + * Примеры: http://host:8080 → http://host:8080/v1 + */ +export function normalizeEvobgpApiBaseUrl(input) { + const raw = String(input || '').trim(); + if (!raw) return ''; + try { + const u = new URL(raw); + let path = (u.pathname || '').replace(/\/+$/, ''); + if (!path || path === '/') { + u.pathname = '/v1'; + } else if (!/\/v1$/i.test(path)) { + u.pathname = `${path}/v1`; + } else { + u.pathname = path; + } + return `${u.origin}${u.pathname}`.replace(/\/+$/, ''); + } catch { + return ''; + } +} + +/** + * База EvoBGP API: VITE_EVOBGP_API_URL → localStorage → в dev только proxy /v1. + * Не используем относительный /v1 в production без настройки — запросы уходили бы на хост SPA, а не на EvoBGP. + */ +export function getEvobgpApiBaseUrl() { + const envUrl = import.meta?.env?.VITE_EVOBGP_API_URL; + if (envUrl && String(envUrl).trim()) { + return normalizeEvobgpApiBaseUrl(String(envUrl).trim()); + } + if (typeof window !== 'undefined') { + const stored = window.localStorage?.getItem(LS_KEY_URL); + if (stored && String(stored).trim()) { + return normalizeEvobgpApiBaseUrl(String(stored).trim()); + } + } + if (import.meta?.env?.DEV) { + return '/v1'; + } + return ''; +} + function getAuthToken() { const envToken = import.meta?.env?.VITE_EVOBGP_API_TOKEN; if (envToken) return String(envToken); if (typeof window === 'undefined') return ''; - return String(window.localStorage?.getItem('evobgp_api_token') || ''); + return String(window.localStorage?.getItem(LS_KEY_TOKEN) || ''); } export const evobgpApi = axios.create({ - baseURL: import.meta?.env?.VITE_EVOBGP_API_URL || '/v1', timeout: DEFAULT_TIMEOUT_MS, }); evobgpApi.interceptors.request.use((config) => { + const base = getEvobgpApiBaseUrl(); + if (!base) { + return Promise.reject( + new Error( + 'EvoBGP API URL не задан. Укажите полный URL API (например http://192.168.0.1:8080) в разделе Communities или задайте VITE_EVOBGP_API_URL при сборке.', + ), + ); + } + config.baseURL = base; const token = getAuthToken(); if (token) { config.headers = config.headers || {};