From d6fc877e90755a6bd88dd59fd31fd1d91ae5ad9d Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 17 Feb 2026 16:00:10 +0700 Subject: [PATCH] feat(SettingsPage): implement traffic interface selection with loading state and error handling for enhanced user interaction --- frontend/src/SettingsPage.jsx | 144 +++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 28 deletions(-) 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() { <>

- Укажите, какие интерфейсы учитывать на странице Расход трафика. - Пустое поле — учитываются все интерфейсы. Имена можно перечислить через запятую или с новой строки. + Отметьте интерфейсы, которые нужно учитывать на странице Расход трафика. + Если ни один не выбран — учитываются все интерфейсы.

-
-
- -