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() { setTrafficInterfaceNamesSelected([...trafficAllInterfaceNames])} - disabled={saving || trafficInterfacesLoading || trafficAllInterfaceNames.length === 0} + onClick={() => setTrafficInterfacesSelected([...trafficAllPairs])} + disabled={saving || trafficInterfacesLoading || trafficAllPairs.length === 0} > Выбрать все setTrafficInterfaceNamesSelected([])} + onClick={() => setTrafficInterfacesSelected([])} disabled={saving} > Снять все @@ -671,28 +710,30 @@ export default function SettingsPage() { )} - {!jh.error && (jh.interfaces?.length ?? 0) > 0 && ( - { - const names = (jh.interfaces || []).map((i) => i.name); - setTrafficInterfaceNamesSelected((prev) => { - const next = new Set(prev); - const allChecked = names.every((n) => next.has(n)); - if (allChecked) names.forEach((n) => next.delete(n)); - else names.forEach((n) => next.add(n)); - return Array.from(next).sort((a, b) => a.localeCompare(b)); - }); - }} - disabled={saving} - title="Выбрать / снять все на этом сервере" - > - {(jh.interfaces || []).every((i) => trafficInterfaceNamesSelected.includes(i.name)) - ? 'Снять все' - : 'Выбрать все'} - - )} + {!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 ( + { + setTrafficInterfacesSelected((prev) => { + const next = prev.filter( + (x) => !pairs.some((p) => p.serverKey === x.serverKey && p.interfaceName === x.interfaceName) + ); + if (!allChecked) next.push(...pairs); + return next; + }); + }} + disabled={saving} + title="Выбрать / снять все на этом сервере" + > + {allChecked ? 'Снять все' : 'Выбрать все'} + + ); + })()} {jh.error && ( @@ -705,27 +746,25 @@ export default function SettingsPage() { )} {!jh.error && (jh.interfaces?.length ?? 0) > 0 && ( - {jh.interfaces.map((iface) => ( - - - { - if (e.target.checked) { - setTrafficInterfaceNamesSelected((prev) => [...prev, iface.name].sort((a, b) => a.localeCompare(b))); - } else { - setTrafficInterfaceNamesSelected((prev) => prev.filter((n) => n !== iface.name)); - } - }} - disabled={saving} - aria-label={`Интерфейс ${iface.name}`} - /> - {iface.name} - - - ))} + {jh.interfaces.map((iface) => { + const sk = getJumphostKey(jh); + const checked = isTrafficInterfaceSelected(sk, iface.name); + return ( + + + setTrafficInterfaceChecked(sk, iface.name, e.target.checked)} + disabled={saving} + aria-label={`Интерфейс ${iface.name}`} + /> + {iface.name} + + + ); + })} )} 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]); // Суммарная статистика по всем отображаемым системам (без ошибок, с данными)