refactor(SettingsPage, TrafficDashboard): rename traffic interface state variables and enhance interface selection logic for improved clarity and functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m1s

This commit is contained in:
2026-02-17 16:17:02 +07:00
parent f8a50581e4
commit d3b21d3dc0
2 changed files with 122 additions and 70 deletions
+97 -58
View File
@@ -74,7 +74,7 @@ export default function SettingsPage() {
const [pingServicesServerId, setPingServicesServerId] = useState(''); const [pingServicesServerId, setPingServicesServerId] = useState('');
const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState(''); const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState('');
const [pingServicesCacheSeconds, setPingServicesCacheSeconds] = useState(''); const [pingServicesCacheSeconds, setPingServicesCacheSeconds] = useState('');
const [trafficInterfaceNamesSelected, setTrafficInterfaceNamesSelected] = useState([]); const [trafficInterfacesSelected, setTrafficInterfacesSelected] = useState([]);
const [trafficJumphosts, setTrafficJumphosts] = useState([]); const [trafficJumphosts, setTrafficJumphosts] = useState([]);
const [trafficInterfacesLoading, setTrafficInterfacesLoading] = useState(false); const [trafficInterfacesLoading, setTrafficInterfacesLoading] = useState(false);
const [trafficInterfacesError, setTrafficInterfacesError] = useState(''); const [trafficInterfacesError, setTrafficInterfacesError] = useState('');
@@ -147,13 +147,39 @@ export default function SettingsPage() {
fetchTrafficInterfaces(); fetchTrafficInterfaces();
}, [activeSection, trafficJumphosts.length, fetchTrafficInterfaces]); }, [activeSection, trafficJumphosts.length, fetchTrafficInterfaces]);
const trafficAllInterfaceNames = useMemo(() => { const getJumphostKey = useCallback((jh) => String(jh?.serverId || jh?.host || jh?.name || ''), []);
const set = new Set();
const trafficAllPairs = useMemo(() => {
const out = [];
for (const jh of trafficJumphosts) { 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)); return out;
}, [trafficJumphosts]); }, [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) => { const goToSection = (id) => {
setActiveSection(id); setActiveSection(id);
@@ -196,10 +222,20 @@ export default function SettingsPage() {
? String(data.pingServicesCacheSeconds) ? String(data.pingServicesCacheSeconds)
: '' : ''
); );
const rawNames = data?.trafficInterfaceNames; const raw = data?.trafficInterfaces;
setTrafficInterfaceNamesSelected( setTrafficInterfacesSelected(
Array.isArray(rawNames) Array.isArray(raw)
? rawNames.filter((n) => n != null).map(String) ? 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 = const e =
@@ -274,8 +310,11 @@ export default function SettingsPage() {
0, 0,
parseInt(pingServicesCacheSeconds, 10) || 0 parseInt(pingServicesCacheSeconds, 10) || 0
), ),
trafficInterfaceNames: Array.isArray(trafficInterfaceNamesSelected) trafficInterfaces: Array.isArray(trafficInterfacesSelected)
? trafficInterfaceNamesSelected.filter(Boolean) ? trafficInterfacesSelected.map((p) => ({
serverKey: p.serverKey,
interfaceName: p.interfaceName,
}))
: [], : [],
}; };
const payload = { settings: mergedSettings, etag }; const payload = { settings: mergedSettings, etag };
@@ -611,15 +650,15 @@ export default function SettingsPage() {
<button <button
type="button" type="button"
className="btn btn-sm btn-outline-secondary" className="btn btn-sm btn-outline-secondary"
onClick={() => setTrafficInterfaceNamesSelected([...trafficAllInterfaceNames])} onClick={() => setTrafficInterfacesSelected([...trafficAllPairs])}
disabled={saving || trafficInterfacesLoading || trafficAllInterfaceNames.length === 0} disabled={saving || trafficInterfacesLoading || trafficAllPairs.length === 0}
> >
Выбрать все Выбрать все
</button> </button>
<button <button
type="button" type="button"
className="btn btn-sm btn-outline-secondary" className="btn btn-sm btn-outline-secondary"
onClick={() => setTrafficInterfaceNamesSelected([])} onClick={() => setTrafficInterfacesSelected([])}
disabled={saving} disabled={saving}
> >
Снять все Снять все
@@ -671,28 +710,30 @@ export default function SettingsPage() {
</div> </div>
)} )}
</div> </div>
{!jh.error && (jh.interfaces?.length ?? 0) > 0 && ( {!jh.error && (jh.interfaces?.length ?? 0) > 0 && (() => {
<button const sk = getJumphostKey(jh);
type="button" const pairs = (jh.interfaces || []).map((i) => ({ serverKey: sk, interfaceName: i.name }));
className="btn btn-sm btn-ghost-secondary" const allChecked = pairs.every((p) => isTrafficInterfaceSelected(p.serverKey, p.interfaceName));
onClick={() => { return (
const names = (jh.interfaces || []).map((i) => i.name); <button
setTrafficInterfaceNamesSelected((prev) => { type="button"
const next = new Set(prev); className="btn btn-sm btn-ghost-secondary"
const allChecked = names.every((n) => next.has(n)); onClick={() => {
if (allChecked) names.forEach((n) => next.delete(n)); setTrafficInterfacesSelected((prev) => {
else names.forEach((n) => next.add(n)); const next = prev.filter(
return Array.from(next).sort((a, b) => a.localeCompare(b)); (x) => !pairs.some((p) => p.serverKey === x.serverKey && p.interfaceName === x.interfaceName)
}); );
}} if (!allChecked) next.push(...pairs);
disabled={saving} return next;
title="Выбрать / снять все на этом сервере" });
> }}
{(jh.interfaces || []).every((i) => trafficInterfaceNamesSelected.includes(i.name)) disabled={saving}
? 'Снять все' title="Выбрать / снять все на этом сервере"
: 'Выбрать все'} >
</button> {allChecked ? 'Снять все' : 'Выбрать все'}
)} </button>
);
})()}
</div> </div>
<div className="card-body"> <div className="card-body">
{jh.error && ( {jh.error && (
@@ -705,27 +746,25 @@ export default function SettingsPage() {
)} )}
{!jh.error && (jh.interfaces?.length ?? 0) > 0 && ( {!jh.error && (jh.interfaces?.length ?? 0) > 0 && (
<div className="row g-2"> <div className="row g-2">
{jh.interfaces.map((iface) => ( {jh.interfaces.map((iface) => {
<div key={iface.name} className="col-12 col-sm-6"> const sk = getJumphostKey(jh);
<label className="form-check"> const checked = isTrafficInterfaceSelected(sk, iface.name);
<input return (
className="form-check-input" <div key={iface.name} className="col-12 col-sm-6">
type="checkbox" <label className="form-check">
checked={trafficInterfaceNamesSelected.includes(iface.name)} <input
onChange={(e) => { className="form-check-input"
if (e.target.checked) { type="checkbox"
setTrafficInterfaceNamesSelected((prev) => [...prev, iface.name].sort((a, b) => a.localeCompare(b))); checked={checked}
} else { onChange={(e) => setTrafficInterfaceChecked(sk, iface.name, e.target.checked)}
setTrafficInterfaceNamesSelected((prev) => prev.filter((n) => n !== iface.name)); disabled={saving}
} aria-label={`Интерфейс ${iface.name}`}
}} />
disabled={saving} <span className="form-check-label font-monospace">{iface.name}</span>
aria-label={`Интерфейс ${iface.name}`} </label>
/> </div>
<span className="form-check-label font-monospace">{iface.name}</span> );
</label> })}
</div>
))}
</div> </div>
)} )}
</div> </div>
+25 -12
View File
@@ -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) { function getTrafficInterfaceFilter(settings) {
const raw = settings?.trafficInterfaceNames; const raw = settings?.trafficInterfaces;
if (!raw) return null; if (!raw || !Array.isArray(raw)) return null;
const arr = Array.isArray(raw) ? raw : (typeof raw === 'string' ? raw.split(/[\n,]+/).map((s) => s.trim()) : []); const pairs = raw
const names = arr.filter(Boolean); .filter((p) => p && (p.serverKey != null || p.serverId != null) && (p.interfaceName != null || p.name != null))
return names.length > 0 ? new Set(names) : 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() { export default function TrafficDashboard() {
@@ -201,12 +207,19 @@ export default function TrafficDashboard() {
const filteredJumphosts = useMemo(() => { const filteredJumphosts = useMemo(() => {
if (!interfaceFilter) return jumphosts; if (!interfaceFilter) return jumphosts;
return jumphosts.map((jh) => ({ return jumphosts.map((jh) => {
...jh, const sk = getJumphostKey(jh);
interfaces: Array.isArray(jh.interfaces) return {
? jh.interfaces.filter((i) => i.name != null && interfaceFilter.has(String(i.name).trim())) ...jh,
: [], interfaces: Array.isArray(jh.interfaces)
})); ? jh.interfaces.filter(
(i) =>
i.name != null &&
interfaceFilter.has(`${sk}\n${String(i.name).trim()}`)
)
: [],
};
});
}, [jumphosts, interfaceFilter]); }, [jumphosts, interfaceFilter]);
// Суммарная статистика по всем отображаемым системам (без ошибок, с данными) // Суммарная статистика по всем отображаемым системам (без ошибок, с данными)