diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx
index 217ec91..3b04508 100644
--- a/frontend/src/SettingsPage.jsx
+++ b/frontend/src/SettingsPage.jsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useMemo } from 'react';
+import { useEffect, useState, useMemo, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import api from './lib/api.js';
import {
@@ -11,6 +11,7 @@ import {
IconWorld,
IconSearch,
IconChartBar,
+ IconRefresh,
} from '@tabler/icons-react';
import FormField from './components/FormField';
import ErrorAlert from './components/ErrorAlert';
@@ -72,7 +73,10 @@ export default function SettingsPage() {
const [pingServicesServerId, setPingServicesServerId] = useState('');
const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState('');
const [pingServicesCacheSeconds, setPingServicesCacheSeconds] = useState('');
- const [trafficInterfaceNames, setTrafficInterfaceNames] = useState('');
+ const [trafficInterfaceNamesSelected, setTrafficInterfaceNamesSelected] = useState([]);
+ const [trafficInterfacesList, setTrafficInterfacesList] = useState([]);
+ const [trafficInterfacesLoading, setTrafficInterfacesLoading] = useState(false);
+ const [trafficInterfacesError, setTrafficInterfacesError] = useState('');
const [serversList, setServersList] = useState([]);
const [sidebarSearch, setSidebarSearch] = useState('');
const [activeSection, setActiveSection] = useState(() => {
@@ -108,6 +112,36 @@ export default function SettingsPage() {
}
}, [location.hash]);
+ const fetchTrafficInterfaces = useCallback(async () => {
+ setTrafficInterfacesLoading(true);
+ setTrafficInterfacesError('');
+ try {
+ const { data } = await api.get('/traffic/interface-stats');
+ const jumphosts = Array.isArray(data?.jumphosts) ? data.jumphosts : [];
+ const namesSet = new Set();
+ for (const jh of jumphosts) {
+ if (Array.isArray(jh.interfaces)) {
+ for (const i of jh.interfaces) {
+ if (i?.name != null && String(i.name).trim()) {
+ namesSet.add(String(i.name).trim());
+ }
+ }
+ }
+ }
+ setTrafficInterfacesList(Array.from(namesSet).sort((a, b) => a.localeCompare(b)));
+ } catch (e) {
+ setTrafficInterfacesError(e?.response?.data?.message || e?.message || 'Не удалось загрузить список интерфейсов');
+ setTrafficInterfacesList([]);
+ } finally {
+ setTrafficInterfacesLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (activeSection !== 'traffic-interfaces' || trafficInterfacesList.length > 0) return;
+ fetchTrafficInterfaces();
+ }, [activeSection, trafficInterfacesList.length, fetchTrafficInterfaces]);
+
const goToSection = (id) => {
setActiveSection(id);
navigate(`/settings#${id}`, { replace: true });
@@ -150,12 +184,10 @@ export default function SettingsPage() {
: ''
);
const rawNames = data?.trafficInterfaceNames;
- setTrafficInterfaceNames(
+ setTrafficInterfaceNamesSelected(
Array.isArray(rawNames)
- ? rawNames.filter((n) => n != null).map(String).join(', ')
- : typeof rawNames === 'string'
- ? rawNames
- : ''
+ ? rawNames.filter((n) => n != null).map(String)
+ : []
);
const e =
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
@@ -229,10 +261,9 @@ export default function SettingsPage() {
0,
parseInt(pingServicesCacheSeconds, 10) || 0
),
- trafficInterfaceNames: (trafficInterfaceNames || '')
- .split(/[\n,]+/)
- .map((s) => s.trim())
- .filter(Boolean),
+ trafficInterfaceNames: Array.isArray(trafficInterfaceNamesSelected)
+ ? trafficInterfaceNamesSelected.filter(Boolean)
+ : [],
};
const payload = { settings: mergedSettings, etag };
const res = await api.post('/ui-settings', payload);
@@ -557,25 +588,82 @@ export default function SettingsPage() {
<>
- Укажите, какие интерфейсы учитывать на странице Расход трафика. - Пустое поле — учитываются все интерфейсы. Имена можно перечислить через запятую или с новой строки. + Отметьте интерфейсы, которые нужно учитывать на странице Расход трафика. + Если ни один не выбран — учитываются все интерфейсы.
-