mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-12-15 16:46:24 +00:00
Update terminal-panel.tsx
This commit is contained in:
@@ -3,7 +3,19 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useEffect, useRef, useState, useCallback } from "react"
|
import { useEffect, useRef, useState, useCallback } from "react"
|
||||||
import { API_PORT } from "@/lib/api-config"
|
import { API_PORT } from "@/lib/api-config"
|
||||||
import { Activity, Trash2, X, Search, Send, Lightbulb, Terminal, Plus, Split, Grid2X2 } from "lucide-react"
|
import {
|
||||||
|
Activity,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
Search,
|
||||||
|
Send,
|
||||||
|
Lightbulb,
|
||||||
|
Terminal,
|
||||||
|
Plus,
|
||||||
|
Split,
|
||||||
|
Grid2X2,
|
||||||
|
GripHorizontal,
|
||||||
|
} from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -124,6 +136,8 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||||
const [layout, setLayout] = useState<"single" | "vertical" | "horizontal" | "grid">("single")
|
const [layout, setLayout] = useState<"single" | "vertical" | "horizontal" | "grid">("single")
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
const [terminalHeight, setTerminalHeight] = useState<number>(500) // altura por defecto en px
|
||||||
|
const [isResizing, setIsResizing] = useState(false)
|
||||||
|
|
||||||
const [searchModalOpen, setSearchModalOpen] = useState(false)
|
const [searchModalOpen, setSearchModalOpen] = useState(false)
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
@@ -134,21 +148,61 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
const [useOnline, setUseOnline] = useState(true)
|
const [useOnline, setUseOnline] = useState(true)
|
||||||
|
|
||||||
const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
||||||
|
const resizeStartY = useRef<number>(0)
|
||||||
const setContainerRef = useCallback(
|
const resizeStartHeight = useRef<number>(0)
|
||||||
(id: string) => (el: HTMLDivElement | null) => {
|
|
||||||
containerRefs.current[id] = el
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMobile(window.innerWidth < 768)
|
setIsMobile(window.innerWidth < 768)
|
||||||
const handleResize = () => setIsMobile(window.innerWidth < 768)
|
const handleResize = () => setIsMobile(window.innerWidth < 768)
|
||||||
window.addEventListener("resize", handleResize)
|
window.addEventListener("resize", handleResize)
|
||||||
|
|
||||||
|
const savedHeight = localStorage.getItem("terminalHeight")
|
||||||
|
if (savedHeight) {
|
||||||
|
setTerminalHeight(Number.parseInt(savedHeight, 10))
|
||||||
|
}
|
||||||
|
|
||||||
return () => window.removeEventListener("resize", handleResize)
|
return () => window.removeEventListener("resize", handleResize)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleResizeStart = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (isMobile) return
|
||||||
|
setIsResizing(true)
|
||||||
|
resizeStartY.current = e.clientY
|
||||||
|
resizeStartHeight.current = terminalHeight
|
||||||
|
e.preventDefault()
|
||||||
|
},
|
||||||
|
[isMobile, terminalHeight],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleResizeMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!isResizing) return
|
||||||
|
|
||||||
|
const deltaY = resizeStartY.current - e.clientY
|
||||||
|
const newHeight = Math.max(200, Math.min(800, resizeStartHeight.current + deltaY))
|
||||||
|
setTerminalHeight(newHeight)
|
||||||
|
},
|
||||||
|
[isResizing],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleResizeEnd = useCallback(() => {
|
||||||
|
if (!isResizing) return
|
||||||
|
setIsResizing(false)
|
||||||
|
localStorage.setItem("terminalHeight", terminalHeight.toString())
|
||||||
|
}, [isResizing, terminalHeight])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResizing) {
|
||||||
|
document.addEventListener("mousemove", handleResizeMove)
|
||||||
|
document.addEventListener("mouseup", handleResizeEnd)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousemove", handleResizeMove)
|
||||||
|
document.removeEventListener("mouseup", handleResizeEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isResizing, handleResizeMove, handleResizeEnd])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminals.length === 0) {
|
if (terminals.length === 0) {
|
||||||
addNewTerminal()
|
addNewTerminal()
|
||||||
@@ -187,18 +241,16 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
|
|
||||||
console.log("[v0] Received parsed examples from server:", data.examples.length)
|
console.log("[v0] Received parsed examples from server:", data.examples.length)
|
||||||
|
|
||||||
// Usar los ejemplos ya parseados del servidor
|
|
||||||
const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({
|
const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({
|
||||||
command: example.command,
|
command: example.command,
|
||||||
description: example.description || "",
|
description: example.description || "",
|
||||||
examples: [example.command], // El comando limpio como ejemplo
|
examples: [example.command],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
setUseOnline(true)
|
setUseOnline(true)
|
||||||
setSearchResults(formattedResults)
|
setSearchResults(formattedResults)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[v0] Error fetching from cheat.sh proxy, using offline commands:", error)
|
console.log("[v0] Error fetching from cheat.sh proxy, using offline commands:", error)
|
||||||
// Mostrar resultados offline si falla
|
|
||||||
const filtered = proxmoxCommands.filter(
|
const filtered = proxmoxCommands.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.cmd.toLowerCase().includes(query.toLowerCase()) ||
|
item.cmd.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
@@ -228,8 +280,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
if (terminals.length >= 4) return
|
if (terminals.length >= 4) return
|
||||||
|
|
||||||
const newId = `terminal-${Date.now()}`
|
const newId = `terminal-${Date.now()}`
|
||||||
// containerRefs.current[newId] = useRef<HTMLDivElement>(null) // No longer needed
|
|
||||||
|
|
||||||
setTerminals((prev) => [
|
setTerminals((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -238,7 +288,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
term: null,
|
term: null,
|
||||||
ws: null,
|
ws: null,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
// containerRef: containerRefs.current[newId], // No longer needed
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
setActiveTerminalId(newId)
|
setActiveTerminalId(newId)
|
||||||
@@ -263,7 +312,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
return filtered
|
return filtered
|
||||||
})
|
})
|
||||||
|
|
||||||
delete containerRefs.current[id] // Clean up the ref
|
delete containerRefs.current[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -323,7 +372,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
const wsUrl = websocketUrl || getWebSocketUrl()
|
const wsUrl = websocketUrl || getWebSocketUrl()
|
||||||
const ws = new WebSocket(wsUrl)
|
const ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
// Ajusta al contenedor y envía tamaño al backend
|
|
||||||
const syncSizeWithBackend = () => {
|
const syncSizeWithBackend = () => {
|
||||||
try {
|
try {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
@@ -346,7 +394,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws } : t)))
|
setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws } : t)))
|
||||||
term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m")
|
term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m")
|
||||||
// Tamaño inicial
|
|
||||||
syncSizeWithBackend()
|
syncSizeWithBackend()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,14 +412,12 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m")
|
term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teclas → backend
|
|
||||||
term.onData((data) => {
|
term.onData((data) => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(data)
|
ws.send(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Solo reaccionamos a cambios de tamaño de la ventana
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
syncSizeWithBackend()
|
syncSizeWithBackend()
|
||||||
}
|
}
|
||||||
@@ -441,11 +486,8 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||||
|
|
||||||
if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) {
|
if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) {
|
||||||
// Solo enviamos el comando sin el \n para que no se ejecute automáticamente
|
|
||||||
// El usuario puede editar el comando y presionar Enter cuando esté listo
|
|
||||||
activeTerminal.ws.send(command)
|
activeTerminal.ws.send(command)
|
||||||
|
|
||||||
// Pequeño delay antes de cerrar el modal para asegurar que el comando se envió
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSearchModalOpen(false)
|
setSearchModalOpen(false)
|
||||||
}, 100)
|
}, 100)
|
||||||
@@ -476,6 +518,18 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-zinc-950 rounded-md overflow-hidden">
|
<div className="flex flex-col h-full bg-zinc-950 rounded-md overflow-hidden">
|
||||||
|
{!isMobile && (
|
||||||
|
<div
|
||||||
|
onMouseDown={handleResizeStart}
|
||||||
|
className={`h-2 bg-zinc-800 hover:bg-blue-600 cursor-ns-resize flex items-center justify-center transition-colors ${
|
||||||
|
isResizing ? "bg-blue-600" : ""
|
||||||
|
}`}
|
||||||
|
title="Arrastra para redimensionar"
|
||||||
|
>
|
||||||
|
<GripHorizontal className="h-4 w-4 text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800">
|
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Activity className="h-5 w-5 text-blue-500" />
|
<Activity className="h-5 w-5 text-blue-500" />
|
||||||
@@ -557,7 +611,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0" style={!isMobile ? { height: `${terminalHeight}px` } : undefined}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<Tabs value={activeTerminalId} onValueChange={setActiveTerminalId} className="h-full flex flex-col">
|
<Tabs value={activeTerminalId} onValueChange={setActiveTerminalId} className="h-full flex flex-col">
|
||||||
<TabsList className="w-full justify-start bg-zinc-900 rounded-none border-b border-zinc-800">
|
<TabsList className="w-full justify-start bg-zinc-900 rounded-none border-b border-zinc-800">
|
||||||
@@ -581,7 +635,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
{terminals.map((terminal) => (
|
{terminals.map((terminal) => (
|
||||||
<TabsContent key={terminal.id} value={terminal.id} className="flex-1 m-0 p-0">
|
<TabsContent key={terminal.id} value={terminal.id} className="flex-1 m-0 p-0">
|
||||||
<div
|
<div
|
||||||
ref={setContainerRef(terminal.id)}
|
ref={(el) => (containerRefs.current[terminal.id] = el)}
|
||||||
className="w-full h-full bg-black overflow-hidden"
|
className="w-full h-full bg-black overflow-hidden"
|
||||||
style={{ height: "calc(100vh - 24rem)" }}
|
style={{ height: "calc(100vh - 24rem)" }}
|
||||||
/>
|
/>
|
||||||
@@ -607,7 +661,10 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div ref={setContainerRef(terminal.id)} className="w-full h-full bg-black pt-7 overflow-hidden" />
|
<div
|
||||||
|
ref={(el) => (containerRefs.current[terminal.id] = el)}
|
||||||
|
className="w-full h-full bg-black pt-7 overflow-hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -702,7 +759,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Powered by cheat.sh */}
|
|
||||||
<div className="text-center py-2">
|
<div className="text-center py-2">
|
||||||
<p className="text-xs text-zinc-500">
|
<p className="text-xs text-zinc-500">
|
||||||
<Lightbulb className="inline-block w-3 h-3 mr-1" />
|
<Lightbulb className="inline-block w-3 h-3 mr-1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user