mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-02-10 17:40:30 -08:00
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { Send, Trash2 } from 'lucide-react'
|
|
|
|
interface ConsoleViewerProps {
|
|
serverId: string
|
|
readOnly?: boolean
|
|
}
|
|
|
|
export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleViewerProps) {
|
|
const [lines, setLines] = useState<string[]>([])
|
|
const [command, setCommand] = useState('')
|
|
const [sending, setSending] = useState(false)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
// Auto-scroll to bottom on new lines
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
|
}
|
|
}, [lines])
|
|
|
|
// SSE log streaming
|
|
useEffect(() => {
|
|
const eventSource = new EventSource(`/api/servers/${serverId}/console`)
|
|
|
|
eventSource.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data)
|
|
const line = data.line || event.data
|
|
setLines(prev => [...prev.slice(-500), line])
|
|
} catch {
|
|
setLines(prev => [...prev.slice(-500), event.data])
|
|
}
|
|
}
|
|
|
|
eventSource.onerror = () => {
|
|
eventSource.close()
|
|
}
|
|
|
|
return () => {
|
|
eventSource.close()
|
|
}
|
|
}, [serverId])
|
|
|
|
const handleSendCommand = async () => {
|
|
if (!command.trim() || sending) return
|
|
|
|
const cmd = command.trim()
|
|
// Show the command in the console
|
|
setLines(prev => [...prev.slice(-500), `> ${cmd}`])
|
|
setCommand('')
|
|
inputRef.current?.focus()
|
|
|
|
setSending(true)
|
|
try {
|
|
const res = await fetch(`/api/servers/${serverId}/console`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ command: cmd }),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json()
|
|
setLines(prev => [...prev.slice(-500), `§c Error: ${data.error || 'Command failed'}`])
|
|
}
|
|
// Output arrives via SSE log stream — no need to parse response
|
|
} catch (error) {
|
|
console.error('Failed to send command:', error)
|
|
setLines(prev => [...prev.slice(-500), '§c Error: Failed to send command'])
|
|
} finally {
|
|
setSending(false)
|
|
}
|
|
}
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
handleSendCommand()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
|
|
{/* Console Output */}
|
|
<div
|
|
ref={containerRef}
|
|
className="flex-1 overflow-y-auto p-4 font-mono text-sm text-gray-300 space-y-0.5 min-h-[400px]"
|
|
>
|
|
{lines.length === 0 ? (
|
|
<p className="text-gray-600 italic">Waiting for server output...</p>
|
|
) : (
|
|
lines.map((line, i) => (
|
|
<div key={i} className="whitespace-pre-wrap break-all hover:bg-gray-900/50">
|
|
{line}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Command Input */}
|
|
{!readOnly && (
|
|
<div className="flex items-center gap-2 p-3 border-t border-gray-700/50 bg-gray-900/50">
|
|
<span className="text-cyan-500 font-mono text-sm">></span>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={command}
|
|
onChange={e => setCommand(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Type a command..."
|
|
className="flex-1 bg-transparent text-gray-100 font-mono text-sm placeholder-gray-600 focus:outline-none"
|
|
/>
|
|
<button
|
|
onClick={() => setLines([])}
|
|
className="p-1.5 rounded text-gray-500 hover:text-gray-300 hover:bg-gray-800 transition-colors"
|
|
aria-label="Clear console"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
<button
|
|
onClick={handleSendCommand}
|
|
disabled={!command.trim() || sending}
|
|
className="p-1.5 rounded text-cyan-400 hover:text-cyan-300 hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
aria-label="Send command"
|
|
>
|
|
<Send size={16} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|