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 [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() {
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => setTrafficInterfaceNamesSelected([...trafficAllInterfaceNames])}
disabled={saving || trafficInterfacesLoading || trafficAllInterfaceNames.length === 0}
onClick={() => setTrafficInterfacesSelected([...trafficAllPairs])}
disabled={saving || trafficInterfacesLoading || trafficAllPairs.length === 0}
>
Выбрать все
</button>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => setTrafficInterfaceNamesSelected([])}
onClick={() => setTrafficInterfacesSelected([])}
disabled={saving}
>
Снять все
@@ -671,28 +710,30 @@ export default function SettingsPage() {
</div>
)}
</div>
{!jh.error && (jh.interfaces?.length ?? 0) > 0 && (
<button
type="button"
className="btn btn-sm btn-ghost-secondary"
onClick={() => {
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))
? 'Снять все'
: 'Выбрать все'}
</button>
)}
{!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 (
<button
type="button"
className="btn btn-sm btn-ghost-secondary"
onClick={() => {
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 ? 'Снять все' : 'Выбрать все'}
</button>
);
})()}
</div>
<div className="card-body">
{jh.error && (
@@ -705,27 +746,25 @@ export default function SettingsPage() {
)}
{!jh.error && (jh.interfaces?.length ?? 0) > 0 && (
<div className="row g-2">
{jh.interfaces.map((iface) => (
<div key={iface.name} className="col-12 col-sm-6">
<label className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={trafficInterfaceNamesSelected.includes(iface.name)}
onChange={(e) => {
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}`}
/>
<span className="form-check-label font-monospace">{iface.name}</span>
</label>
</div>
))}
{jh.interfaces.map((iface) => {
const sk = getJumphostKey(jh);
const checked = isTrafficInterfaceSelected(sk, iface.name);
return (
<div key={iface.name} className="col-12 col-sm-6">
<label className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={checked}
onChange={(e) => setTrafficInterfaceChecked(sk, iface.name, e.target.checked)}
disabled={saving}
aria-label={`Интерфейс ${iface.name}`}
/>
<span className="form-check-label font-monospace">{iface.name}</span>
</label>
</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) {
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]);
// Суммарная статистика по всем отображаемым системам (без ошибок, с данными)