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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user