All checks were successful
continuous-integration/drone/push Build is passing
122 lines
3.6 KiB
JavaScript
122 lines
3.6 KiB
JavaScript
import { useState, useEffect, useRef } from 'react';
|
|
import { Send } from 'lucide-react';
|
|
import axios from 'axios';
|
|
import { API_URL, WS_URL } from '../config';
|
|
|
|
export default function Console({ serverName, token }) {
|
|
const [logs, setLogs] = useState([]);
|
|
const [command, setCommand] = useState('');
|
|
const logsEndRef = useRef(null);
|
|
const wsRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
setLogs([]);
|
|
|
|
const ws = new WebSocket(`${WS_URL}/ws/servers/${serverName}/console`);
|
|
|
|
ws.onopen = () => {
|
|
console.log('WebSocket подключен');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
setLogs((prev) => [...prev, event.data]);
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
console.error('WebSocket ошибка:', error);
|
|
};
|
|
|
|
wsRef.current = ws;
|
|
|
|
return () => {
|
|
ws.close();
|
|
};
|
|
}, [serverName]);
|
|
|
|
useEffect(() => {
|
|
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [logs]);
|
|
|
|
const sendCommand = async (e) => {
|
|
e.preventDefault();
|
|
if (!command.trim()) return;
|
|
|
|
try {
|
|
await axios.post(
|
|
`${API_URL}/api/servers/${serverName}/command`,
|
|
{ command: command.trim() },
|
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
);
|
|
setCommand('');
|
|
} catch (error) {
|
|
console.error('Ошибка отправки команды:', error);
|
|
alert(error.response?.data?.detail || 'Ошибка отправки команды');
|
|
}
|
|
};
|
|
|
|
// Функция для раскраски логов
|
|
const colorizeLog = (log) => {
|
|
// INFO - зеленый
|
|
if (log.includes('[INFO]') || log.includes('Done (')) {
|
|
return <span className="text-green-400">{log}</span>;
|
|
}
|
|
// WARN - желтый
|
|
if (log.includes('[WARN]') || log.includes('WARNING')) {
|
|
return <span className="text-yellow-400">{log}</span>;
|
|
}
|
|
// ERROR - красный
|
|
if (log.includes('[ERROR]') || log.includes('Exception')) {
|
|
return <span className="text-red-400">{log}</span>;
|
|
}
|
|
// Время - серый
|
|
if (log.match(/^\[\d{2}:\d{2}:\d{2}\]/)) {
|
|
const time = log.match(/^\[\d{2}:\d{2}:\d{2}\]/)[0];
|
|
const rest = log.substring(time.length);
|
|
return (
|
|
<>
|
|
<span className="text-gray-500">{time}</span>
|
|
<span className="text-gray-300">{rest}</span>
|
|
</>
|
|
);
|
|
}
|
|
// Обычный текст
|
|
return <span className="text-gray-300">{log}</span>;
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Консоль */}
|
|
<div className="console-terminal flex-1 overflow-y-auto min-h-[400px] max-h-[600px]">
|
|
{logs.length === 0 ? (
|
|
<div className="text-gray-500">Консоль пуста. Запустите сервер для просмотра логов.</div>
|
|
) : (
|
|
logs.map((log, index) => (
|
|
<div key={index} className="whitespace-pre-wrap leading-relaxed">
|
|
{colorizeLog(log)}
|
|
</div>
|
|
))
|
|
)}
|
|
<div ref={logsEndRef} />
|
|
</div>
|
|
|
|
{/* Поле ввода команды */}
|
|
<form onSubmit={sendCommand} className="border-t border-dark-700 p-4 flex gap-2 bg-dark-850">
|
|
<input
|
|
type="text"
|
|
value={command}
|
|
onChange={(e) => setCommand(e.target.value)}
|
|
placeholder="Введите команду..."
|
|
className="input flex-1"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="btn-success flex items-center gap-2"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
Отправить
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|