Files
vpn-proxy/src/web/components/LogsPanel.jsx
Dmitriy Petrov 8789496ae6 feat: добавлены компоненты для управления конфигурацией и логами
Добавлены новые компоненты для отображения и управления конфигурацией, логами и правилами маршрутизации. Реализована логика для работы с API, включая запросы на получение и сохранение данных. Также добавлены шаблоны правил и утилиты для валидации.

Refs: None
2026-05-08 18:23:29 +03:00

76 lines
2.5 KiB
JavaScript

import React, { useEffect, useRef, useState } from 'react';
import { formatTime } from '../utils/format.js';
export function LogsPanel() {
const [entries, setEntries] = useState([]);
const [paused, setPaused] = useState(false);
const [filter, setFilter] = useState('all');
const containerRef = useRef(null);
const pausedRef = useRef(false);
useEffect(() => {
pausedRef.current = paused;
}, [paused]);
useEffect(() => {
const source = new EventSource('/api/logs/stream');
source.onmessage = (event) => {
if (pausedRef.current) return;
try {
const entry = JSON.parse(event.data);
setEntries((prev) => {
const next = [...prev, entry];
if (next.length > 500) next.splice(0, next.length - 500);
return next;
});
} catch {}
};
source.onerror = () => {
// EventSource сам делает реконнект
};
return () => source.close();
}, []);
useEffect(() => {
if (paused || !containerRef.current) return;
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}, [entries, paused]);
const filtered = entries.filter((entry) => filter === 'all' || entry.level === filter);
return (
<section className="panel logs-panel">
<div className="rules-header">
<div className="section-title">
<span>5</span>
<h2>Логи sing-box</h2>
</div>
<div className="rules-actions">
<select value={filter} onChange={(event) => setFilter(event.target.value)}>
<option value="all">все уровни</option>
<option value="info">info</option>
<option value="error">error</option>
</select>
<button className="ghost-button" type="button" onClick={() => setPaused((p) => !p)}>
{paused ? 'Возобновить' : 'Пауза'}
</button>
<button className="ghost-button" type="button" onClick={() => setEntries([])}>
Очистить
</button>
</div>
</div>
<div ref={containerRef} className="logs-stream">
{filtered.length === 0 && <p className="empty">Логов пока нет.</p>}
{filtered.map((entry, index) => (
<p key={`${entry.ts}-${index}`} className={`log-line log-${entry.level}`}>
<span className="log-time">{formatTime(entry.ts)}</span>
<span className="log-level">{entry.level}</span>
<span className="log-text">{entry.line}</span>
</p>
))}
</div>
</section>
);
}