feat: добавлены компоненты для управления конфигурацией и логами
Добавлены новые компоненты для отображения и управления конфигурацией, логами и правилами маршрутизации. Реализована логика для работы с API, включая запросы на получение и сохранение данных. Также добавлены шаблоны правил и утилиты для валидации. Refs: None
This commit is contained in:
75
src/web/components/LogsPanel.jsx
Normal file
75
src/web/components/LogsPanel.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user