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