Добавлены новые компоненты для отображения и управления конфигурацией, логами и правилами маршрутизации. Реализована логика для работы с API, включая запросы на получение и сохранение данных. Также добавлены шаблоны правил и утилиты для валидации. Refs: None
76 lines
2.5 KiB
JavaScript
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>
|
|
);
|
|
}
|