import { Head, usePage } from '@inertiajs/react'; import { Server } from '@/types/server'; import ServerLayout from '@/layouts/server/layout'; import HeaderContainer from '@/components/header-container'; import Heading from '@/components/heading'; import { Button } from '@/components/ui/button'; import { BookOpenIcon, Trash2, Square, LoaderCircleIcon } from 'lucide-react'; import Container from '@/components/container'; import { useState, useRef, FormEvent, useCallback } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import LogOutput from '@/components/log-output'; export default function Console() { const page = usePage<{ server: Server; }>(); const [user, setUser] = useState(page.props.server.ssh_user); const [running, setRunning] = useState(false); const [dir, setDir] = useState('~'); const [command, setCommand] = useState(''); const [output, setOutput] = useState(''); const [shellPrefix, setShellPrefix] = useState(''); const [clearAfterCommand] = useState(false); const [initialized, setInitialized] = useState(false); const outputRef = useRef(null); const commandRef = useRef(null); const updateShellPrefix = useCallback( (currentUser: string, currentDir: string) => { setShellPrefix(`${currentUser}@${page.props.server.name}:${currentDir}$`); }, [page.props.server.name], ); const focusCommand = () => { commandRef.current?.focus(); }; const getWorkingDir = useCallback( async (currentUser: string) => { try { const response = await fetch(route('console.working-dir', { server: page.props.server.id })); if (response.ok) { const data = await response.json(); setDir(data.dir); updateShellPrefix(currentUser, data.dir); return data.dir; } } catch (error) { console.error('Failed to get working directory:', error); } return dir; }, [page.props.server.id, dir, updateShellPrefix], ); const scrollToBottom = () => { setTimeout(() => { if (outputRef.current) { outputRef.current.scrollTop = outputRef.current.scrollHeight; } }, 100); }; const clearOutput = useCallback(() => { if (!running) { setOutput(''); } }, [running]); const initialize = useCallback(async () => { if (initialized) return; const currentDir = await getWorkingDir(user); updateShellPrefix(user, currentDir); focusCommand(); const handleKeydown = (event: KeyboardEvent) => { if (event.ctrlKey && event.key === 'l') { event.preventDefault(); if (!running) { clearOutput(); } } }; const handleMouseUp = () => { if (window.getSelection()?.toString()) { return; } focusCommand(); }; document.addEventListener('keydown', handleKeydown); outputRef.current?.addEventListener('mouseup', handleMouseUp); setInitialized(true); return () => { document.removeEventListener('keydown', handleKeydown); outputRef.current?.removeEventListener('mouseup', handleMouseUp); }; }, [user, updateShellPrefix, initialized, running, clearOutput, getWorkingDir]); const handleUserChange = async (newUser: string) => { setUser(newUser); const currentDir = await getWorkingDir(newUser); updateShellPrefix(newUser, currentDir); }; const run = async () => { if (!command.trim() || running) return; setRunning(true); const commandOutput = `${shellPrefix} ${command}\n`; const cancelled = false; if (clearAfterCommand) { setOutput(commandOutput); } else { setOutput((prev) => prev + commandOutput); } scrollToBottom(); try { const response = await fetch(route('console.run', { server: page.props.server.id }), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': page.props.csrf_token as string, }, body: JSON.stringify({ user, command, }), }); setCommand(''); if (response.body) { const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { if (cancelled) { await reader.cancel(); setOutput((prev) => prev + '\nStopped!'); break; } const { value, done } = await reader.read(); if (done) break; const textChunk = decoder.decode(value, { stream: true }); setOutput((prev) => prev + textChunk); scrollToBottom(); } } setOutput((prev) => prev + '\n'); await getWorkingDir(user); } catch (error) { console.error('Command execution failed:', error); setOutput((prev) => prev + '\nError executing command\n'); } finally { setRunning(false); setTimeout(() => focusCommand(), 100); } }; const stop = () => { setRunning(false); }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); run(); }; // Initialize on first render if (!initialized) { initialize(); } return (
{!running && ( )} {running && ( )}
{output}
{!running ? (
{shellPrefix} setCommand(e.target.value)} className="ml-2 h-auto flex-grow border-0 bg-transparent! p-0 shadow-none ring-0 outline-none focus:ring-0 focus:outline-none focus-visible:ring-0" autoComplete="off" autoFocus />
); }