feat(SettingsPage, TrafficDashboard): add traffic interface settings and filtering for enhanced analytics functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s

This commit is contained in:
2026-02-17 15:56:17 +07:00
parent e071d5060f
commit e0c7ec32d6
2 changed files with 78 additions and 4 deletions
+47
View File
@@ -10,6 +10,7 @@ import {
IconChartPie,
IconWorld,
IconSearch,
IconChartBar,
} from '@tabler/icons-react';
import FormField from './components/FormField';
import ErrorAlert from './components/ErrorAlert';
@@ -42,6 +43,12 @@ const SIDEBAR_GROUPS = [
{ id: 'ptr-zone', title: 'PTR зона', icon: IconWorld },
],
},
{
title: 'Аналитика',
items: [
{ id: 'traffic-interfaces', title: 'Настройка Аналитики', icon: IconChartBar },
],
},
];
const SIDEBAR_SECTIONS = SIDEBAR_GROUPS.flatMap((g) => g.items);
@@ -65,6 +72,7 @@ export default function SettingsPage() {
const [pingServicesServerId, setPingServicesServerId] = useState('');
const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState('');
const [pingServicesCacheSeconds, setPingServicesCacheSeconds] = useState('');
const [trafficInterfaceNames, setTrafficInterfaceNames] = useState('');
const [serversList, setServersList] = useState([]);
const [sidebarSearch, setSidebarSearch] = useState('');
const [activeSection, setActiveSection] = useState(() => {
@@ -141,6 +149,14 @@ export default function SettingsPage() {
? String(data.pingServicesCacheSeconds)
: ''
);
const rawNames = data?.trafficInterfaceNames;
setTrafficInterfaceNames(
Array.isArray(rawNames)
? rawNames.filter((n) => n != null).map(String).join(', ')
: typeof rawNames === 'string'
? rawNames
: ''
);
const e =
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
setEtag(e ? String(e) : '');
@@ -213,6 +229,10 @@ export default function SettingsPage() {
0,
parseInt(pingServicesCacheSeconds, 10) || 0
),
trafficInterfaceNames: (trafficInterfaceNames || '')
.split(/[\n,]+/)
.map((s) => s.trim())
.filter(Boolean),
};
const payload = { settings: mergedSettings, etag };
const res = await api.post('/ui-settings', payload);
@@ -532,6 +552,33 @@ export default function SettingsPage() {
</div>
</>
)}
{activeSection === 'traffic-interfaces' && (
<>
<SectionHeading title="Настройка Аналитики" icon={IconChartBar} />
<p className="text-muted mb-3">
Укажите, какие интерфейсы учитывать на странице <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>
</div>
</>
)}
</div>
</div>
</div>
+31 -4
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import api from './lib/api.js';
import {
IconChartPie,
@@ -158,17 +158,34 @@ function JumphostCard({ jumphost }) {
);
}
/** Нормализовать список имён интерфейсов из настроек (массив, пустой = все) */
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;
}
export default function TrafficDashboard() {
const [jumphosts, setJumphosts] = useState([]);
const [uiSettings, setUiSettings] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const interfaceFilter = getTrafficInterfaceFilter(uiSettings);
const fetchStats = useCallback(async () => {
setLoading(true);
setError(null);
try {
const { data } = await api.get('/traffic/interface-stats');
const [statsRes, settingsRes] = await Promise.all([
api.get('/traffic/interface-stats'),
api.get('/ui-settings').catch(() => ({ data: {} })),
]);
const data = statsRes?.data || {};
setJumphosts(Array.isArray(data?.jumphosts) ? data.jumphosts : []);
setUiSettings(settingsRes?.data || {});
} catch (e) {
console.error('Traffic stats:', e);
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику');
@@ -182,9 +199,19 @@ export default function TrafficDashboard() {
fetchStats();
}, [fetchStats]);
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()))
: [],
}));
}, [jumphosts, interfaceFilter]);
// Суммарная статистика по всем отображаемым системам (без ошибок, с данными)
const summary = (() => {
const withData = (jumphosts || []).filter((jh) => !jh.error && Array.isArray(jh.interfaces) && jh.interfaces.length > 0);
const withData = (filteredJumphosts || []).filter((jh) => !jh.error && Array.isArray(jh.interfaces) && jh.interfaces.length > 0);
let totalBytes = 0;
let totalRx = 0;
let totalTx = 0;
@@ -334,7 +361,7 @@ export default function TrafficDashboard() {
</div>
)}
<div className="row row-cards g-3">
{jumphosts.map((jh) => (
{filteredJumphosts.map((jh) => (
<div key={jh.serverId || jh.host || jh.name} className="col-12 col-md-6 col-xl-4">
<JumphostCard jumphost={jh} />
</div>