feat(SettingsPage): implement traffic interface selection with loading state and error handling for enhanced user interaction
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m4s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m4s
This commit is contained in:
+116
-28
@@ -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() {
|
||||
<>
|
||||
<SectionHeading title="Настройка Аналитики" icon={IconChartBar} />
|
||||
<p className="text-muted mb-3">
|
||||
Укажите, какие интерфейсы учитывать на странице <strong>Расход трафика</strong>.
|
||||
Пустое поле — учитываются все интерфейсы. Имена можно перечислить через запятую или с новой строки.
|
||||
Отметьте интерфейсы, которые нужно учитывать на странице <strong>Расход трафика</strong>.
|
||||
Если ни один не выбран — учитываются все интерфейсы.
|
||||
</p>
|
||||
<div className="row g-2">
|
||||
<div className="col-12">
|
||||
<label className="form-label">Учитывать интерфейсы</label>
|
||||
<textarea
|
||||
className="form-control font-monospace"
|
||||
rows={4}
|
||||
value={trafficInterfaceNames}
|
||||
onChange={(e) => setTrafficInterfaceNames(e.target.value)}
|
||||
placeholder="ether1, ether2, pppoe-out1"
|
||||
disabled={saving}
|
||||
aria-label="Список имён интерфейсов"
|
||||
/>
|
||||
<div className="form-text">
|
||||
Например: ether1, ether2, pppoe-out1. Пусто — все интерфейсы.
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="form-label d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<span>Учитывать интерфейсы</span>
|
||||
<span className="d-flex align-items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => setTrafficInterfaceNamesSelected([...trafficInterfacesList])}
|
||||
disabled={saving || trafficInterfacesLoading || trafficInterfacesList.length === 0}
|
||||
>
|
||||
Выбрать все
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => setTrafficInterfaceNamesSelected([])}
|
||||
disabled={saving}
|
||||
>
|
||||
Снять все
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-ghost-secondary btn-icon"
|
||||
onClick={fetchTrafficInterfaces}
|
||||
disabled={saving || trafficInterfacesLoading}
|
||||
title="Обновить список интерфейсов"
|
||||
aria-label="Обновить список"
|
||||
>
|
||||
<IconRefresh size={18} className={trafficInterfacesLoading ? 'spin' : ''} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{trafficInterfacesError && (
|
||||
<div className="alert alert-warning py-2 mb-2">
|
||||
{trafficInterfacesError}
|
||||
</div>
|
||||
)}
|
||||
{trafficInterfacesLoading && trafficInterfacesList.length === 0 && (
|
||||
<div className="text-muted py-4 d-flex align-items-center gap-2">
|
||||
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||
Загрузка списка интерфейсов с роутеров…
|
||||
</div>
|
||||
)}
|
||||
{!trafficInterfacesLoading && trafficInterfacesList.length === 0 && !trafficInterfacesError && (
|
||||
<div className="text-muted py-3">
|
||||
Нет доступных интерфейсов. Добавьте jumphost-серверы с MikroTik API и нажмите «Обновить».
|
||||
</div>
|
||||
)}
|
||||
{trafficInterfacesList.length > 0 && (
|
||||
<div className="row g-2 mt-1">
|
||||
{trafficInterfacesList.map((name) => (
|
||||
<div key={name} className="col-12 col-sm-6 col-md-4 col-lg-3">
|
||||
<label className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={trafficInterfaceNamesSelected.includes(name)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setTrafficInterfaceNamesSelected((prev) => [...prev, name].sort((a, b) => a.localeCompare(b)));
|
||||
} else {
|
||||
setTrafficInterfaceNamesSelected((prev) => prev.filter((n) => n !== name));
|
||||
}
|
||||
}}
|
||||
disabled={saving}
|
||||
aria-label={`Интерфейс ${name}`}
|
||||
/>
|
||||
<span className="form-check-label font-monospace">{name}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user