diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index e0670b2..5063e73 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -74,7 +74,7 @@ export default function SettingsPage() { const [pingServicesServerId, setPingServicesServerId] = useState(''); const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState(''); const [pingServicesCacheSeconds, setPingServicesCacheSeconds] = useState(''); - const [trafficInterfaceNamesSelected, setTrafficInterfaceNamesSelected] = useState([]); + const [trafficInterfacesSelected, setTrafficInterfacesSelected] = useState([]); const [trafficJumphosts, setTrafficJumphosts] = useState([]); const [trafficInterfacesLoading, setTrafficInterfacesLoading] = useState(false); const [trafficInterfacesError, setTrafficInterfacesError] = useState(''); @@ -147,13 +147,39 @@ export default function SettingsPage() { fetchTrafficInterfaces(); }, [activeSection, trafficJumphosts.length, fetchTrafficInterfaces]); - const trafficAllInterfaceNames = useMemo(() => { - const set = new Set(); + const getJumphostKey = useCallback((jh) => String(jh?.serverId || jh?.host || jh?.name || ''), []); + + const trafficAllPairs = useMemo(() => { + const out = []; for (const jh of trafficJumphosts) { - for (const i of jh.interfaces || []) set.add(i.name); + const sk = getJumphostKey(jh); + for (const i of jh.interfaces || []) { + if (i?.name) out.push({ serverKey: sk, interfaceName: i.name }); + } } - return Array.from(set).sort((a, b) => a.localeCompare(b)); - }, [trafficJumphosts]); + return out; + }, [trafficJumphosts, getJumphostKey]); + + const isTrafficInterfaceSelected = useCallback( + (serverKey, interfaceName) => + trafficInterfacesSelected.some( + (p) => p.serverKey === serverKey && p.interfaceName === interfaceName + ), + [trafficInterfacesSelected] + ); + + const setTrafficInterfaceChecked = useCallback( + (serverKey, interfaceName, checked) => { + setTrafficInterfacesSelected((prev) => { + const next = prev.filter( + (p) => !(p.serverKey === serverKey && p.interfaceName === interfaceName) + ); + if (checked) next.push({ serverKey, interfaceName }); + return next; + }); + }, + [] + ); const goToSection = (id) => { setActiveSection(id); @@ -196,10 +222,20 @@ export default function SettingsPage() { ? String(data.pingServicesCacheSeconds) : '' ); - const rawNames = data?.trafficInterfaceNames; - setTrafficInterfaceNamesSelected( - Array.isArray(rawNames) - ? rawNames.filter((n) => n != null).map(String) + const raw = data?.trafficInterfaces; + setTrafficInterfacesSelected( + Array.isArray(raw) + ? raw + .filter( + (p) => + p && + (p.serverKey != null || p.serverId != null) && + (p.interfaceName != null || p.name != null) + ) + .map((p) => ({ + serverKey: String(p.serverKey ?? p.serverId ?? ''), + interfaceName: String(p.interfaceName ?? p.name ?? ''), + })) : [] ); const e = @@ -274,8 +310,11 @@ export default function SettingsPage() { 0, parseInt(pingServicesCacheSeconds, 10) || 0 ), - trafficInterfaceNames: Array.isArray(trafficInterfaceNamesSelected) - ? trafficInterfaceNamesSelected.filter(Boolean) + trafficInterfaces: Array.isArray(trafficInterfacesSelected) + ? trafficInterfacesSelected.map((p) => ({ + serverKey: p.serverKey, + interfaceName: p.interfaceName, + })) : [], }; const payload = { settings: mergedSettings, etag }; @@ -611,15 +650,15 @@ export default function SettingsPage() { - )} + {!jh.error && (jh.interfaces?.length ?? 0) > 0 && (() => { + const sk = getJumphostKey(jh); + const pairs = (jh.interfaces || []).map((i) => ({ serverKey: sk, interfaceName: i.name })); + const allChecked = pairs.every((p) => isTrafficInterfaceSelected(p.serverKey, p.interfaceName)); + return ( + + ); + })()}
{jh.error && ( @@ -705,27 +746,25 @@ export default function SettingsPage() { )} {!jh.error && (jh.interfaces?.length ?? 0) > 0 && (
- {jh.interfaces.map((iface) => ( -
- -
- ))} + {jh.interfaces.map((iface) => { + const sk = getJumphostKey(jh); + const checked = isTrafficInterfaceSelected(sk, iface.name); + return ( +
+ +
+ ); + })}
)}
diff --git a/frontend/src/TrafficDashboard.jsx b/frontend/src/TrafficDashboard.jsx index c9e770a..d33dd23 100644 --- a/frontend/src/TrafficDashboard.jsx +++ b/frontend/src/TrafficDashboard.jsx @@ -158,13 +158,19 @@ function JumphostCard({ jumphost }) { ); } -/** Нормализовать список имён интерфейсов из настроек (массив, пустой = все) */ +/** Ключ jumphost для сопоставления с настройками (как в SettingsPage) */ +function getJumphostKey(jh) { + return String(jh?.serverId ?? jh?.host ?? jh?.name ?? ''); +} + +/** Множество выбранных пар (serverKey + interfaceName). Пустое = учитывать все интерфейсы. */ function getTrafficInterfaceFilter(settings) { - const raw = settings?.trafficInterfaceNames; - if (!raw) return null; - const arr = Array.isArray(raw) ? raw : (typeof raw === 'string' ? raw.split(/[\n,]+/).map((s) => s.trim()) : []); - const names = arr.filter(Boolean); - return names.length > 0 ? new Set(names) : null; + const raw = settings?.trafficInterfaces; + if (!raw || !Array.isArray(raw)) return null; + const pairs = raw + .filter((p) => p && (p.serverKey != null || p.serverId != null) && (p.interfaceName != null || p.name != null)) + .map((p) => `${String(p.serverKey ?? p.serverId ?? '')}\n${String(p.interfaceName ?? p.name ?? '')}`); + return pairs.length > 0 ? new Set(pairs) : null; } export default function TrafficDashboard() { @@ -201,12 +207,19 @@ export default function TrafficDashboard() { const filteredJumphosts = useMemo(() => { if (!interfaceFilter) return jumphosts; - return jumphosts.map((jh) => ({ - ...jh, - interfaces: Array.isArray(jh.interfaces) - ? jh.interfaces.filter((i) => i.name != null && interfaceFilter.has(String(i.name).trim())) - : [], - })); + return jumphosts.map((jh) => { + const sk = getJumphostKey(jh); + return { + ...jh, + interfaces: Array.isArray(jh.interfaces) + ? jh.interfaces.filter( + (i) => + i.name != null && + interfaceFilter.has(`${sk}\n${String(i.name).trim()}`) + ) + : [], + }; + }); }, [jumphosts, interfaceFilter]); // Суммарная статистика по всем отображаемым системам (без ошибок, с данными)