mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-22 13:36:17 +00:00
Update AppImage
This commit is contained in:
@@ -632,8 +632,10 @@ export function ProxmoxDashboard() {
|
|||||||
<Hardware key={`hardware-${componentKey}`} />
|
<Hardware key={`hardware-${componentKey}`} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="terminal" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="terminal" className="mt-0">
|
||||||
|
<div className="h-full">
|
||||||
<TerminalPanel key={`terminal-${componentKey}`} />
|
<TerminalPanel key={`terminal-${componentKey}`} />
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { API_PORT } from "@/lib/api-config"
|
import { API_PORT } from "@/lib/api-config"
|
||||||
|
import { Trash2, X, Send, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Activity } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
|
||||||
type TerminalPanelProps = {
|
type TerminalPanelProps = {
|
||||||
websocketUrl?: string
|
websocketUrl?: string
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWebSocketUrl(): string {
|
function getWebSocketUrl(): string {
|
||||||
@@ -25,7 +29,7 @@ function getWebSocketUrl(): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) => {
|
export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onClose }) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const termRef = useRef<any>(null)
|
const termRef = useRef<any>(null)
|
||||||
const fitAddonRef = useRef<any>(null)
|
const fitAddonRef = useRef<any>(null)
|
||||||
@@ -33,6 +37,17 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) =>
|
|||||||
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
|
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
|
||||||
|
|
||||||
const [xtermLoaded, setXtermLoaded] = useState(false)
|
const [xtermLoaded, setXtermLoaded] = useState(false)
|
||||||
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
|
const [mobileInput, setMobileInput] = useState("")
|
||||||
|
const [lastKeyPressed, setLastKeyPressed] = useState<string | null>(null)
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMobile(window.innerWidth < 768)
|
||||||
|
const handleResize = () => setIsMobile(window.innerWidth < 768)
|
||||||
|
window.addEventListener("resize", handleResize)
|
||||||
|
return () => window.removeEventListener("resize", handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return
|
if (typeof window === "undefined") return
|
||||||
@@ -48,11 +63,35 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) =>
|
|||||||
console.log("[v0] TerminalPanel: Initializing terminal")
|
console.log("[v0] TerminalPanel: Initializing terminal")
|
||||||
|
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
fontFamily: "monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
|
||||||
fontSize: 13,
|
fontSize: isMobile ? 11 : 13,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
scrollback: 2000,
|
scrollback: 2000,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
|
cols: 150,
|
||||||
|
rows: 30,
|
||||||
|
theme: {
|
||||||
|
background: "#0d1117",
|
||||||
|
foreground: "#e6edf3",
|
||||||
|
cursor: "#58a6ff",
|
||||||
|
cursorAccent: "#0d1117",
|
||||||
|
black: "#484f58",
|
||||||
|
red: "#f85149",
|
||||||
|
green: "#3fb950",
|
||||||
|
yellow: "#d29922",
|
||||||
|
blue: "#58a6ff",
|
||||||
|
magenta: "#bc8cff",
|
||||||
|
cyan: "#39d353",
|
||||||
|
white: "#b1bac4",
|
||||||
|
brightBlack: "#6e7681",
|
||||||
|
brightRed: "#ff7b72",
|
||||||
|
brightGreen: "#56d364",
|
||||||
|
brightYellow: "#e3b341",
|
||||||
|
brightBlue: "#79c0ff",
|
||||||
|
brightMagenta: "#d2a8ff",
|
||||||
|
brightCyan: "#56d364",
|
||||||
|
brightWhite: "#f0f6fc",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon()
|
||||||
@@ -73,6 +112,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) =>
|
|||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log("[v0] TerminalPanel: WebSocket connected")
|
console.log("[v0] TerminalPanel: WebSocket connected")
|
||||||
|
setIsConnected(true)
|
||||||
term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m")
|
term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +122,13 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) =>
|
|||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error("[v0] TerminalPanel: WebSocket error:", error)
|
console.error("[v0] TerminalPanel: WebSocket error:", error)
|
||||||
|
setIsConnected(false)
|
||||||
term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m")
|
term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m")
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
console.log("[v0] TerminalPanel: WebSocket closed")
|
console.log("[v0] TerminalPanel: WebSocket closed")
|
||||||
|
setIsConnected(false)
|
||||||
term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m")
|
term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,40 +157,41 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) =>
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[v0] TerminalPanel: Failed to load xterm:", error)
|
console.error("[v0] TerminalPanel: Failed to load xterm:", error)
|
||||||
})
|
})
|
||||||
}, [websocketUrl])
|
}, [websocketUrl, isMobile])
|
||||||
|
|
||||||
const sendSequence = (seq: string) => {
|
const sendSequence = (seq: string, keyName?: string) => {
|
||||||
const term = termRef.current
|
const term = termRef.current
|
||||||
const ws = wsRef.current
|
const ws = wsRef.current
|
||||||
if (!term || !ws || ws.readyState !== WebSocket.OPEN) return
|
if (!term || !ws || ws.readyState !== WebSocket.OPEN) return
|
||||||
ws.send(seq)
|
ws.send(seq)
|
||||||
|
if (keyName) {
|
||||||
|
setLastKeyPressed(keyName)
|
||||||
|
setTimeout(() => setLastKeyPressed(null), 2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyButton = (key: string) => {
|
const handleKeyButton = (key: string) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "UP":
|
case "UP":
|
||||||
sendSequence("\x1b[A")
|
sendSequence("\x1b[A", "↑")
|
||||||
break
|
break
|
||||||
case "DOWN":
|
case "DOWN":
|
||||||
sendSequence("\x1b[B")
|
sendSequence("\x1b[B", "↓")
|
||||||
break
|
break
|
||||||
case "RIGHT":
|
case "RIGHT":
|
||||||
sendSequence("\x1b[C")
|
sendSequence("\x1b[C", "→")
|
||||||
break
|
break
|
||||||
case "LEFT":
|
case "LEFT":
|
||||||
sendSequence("\x1b[D")
|
sendSequence("\x1b[D", "←")
|
||||||
break
|
break
|
||||||
case "ESC":
|
case "ESC":
|
||||||
sendSequence("\x1b")
|
sendSequence("\x1b", "ESC")
|
||||||
break
|
break
|
||||||
case "TAB":
|
case "TAB":
|
||||||
sendSequence("\t")
|
sendSequence("\t", "TAB")
|
||||||
break
|
|
||||||
case "ENTER":
|
|
||||||
sendSequence("\r")
|
|
||||||
break
|
break
|
||||||
case "CTRL_C":
|
case "CTRL_C":
|
||||||
sendSequence("\x03")
|
sendSequence("\x03", "CTRL+C")
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -199,44 +242,182 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl }) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
const term = termRef.current
|
||||||
|
if (!term) return
|
||||||
|
term.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
const ws = wsRef.current
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
if (onClose) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMobileInputSend = () => {
|
||||||
|
if (!mobileInput.trim()) return
|
||||||
|
const ws = wsRef.current
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(mobileInput)
|
||||||
|
setLastKeyPressed(mobileInput)
|
||||||
|
setTimeout(() => setLastKeyPressed(null), 2000)
|
||||||
|
}
|
||||||
|
setMobileInput("")
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full">
|
<div className="flex flex-col h-[calc(100vh-16rem)] min-h-[500px] w-full">
|
||||||
|
<div className="flex items-center justify-between gap-2 px-3 py-2 bg-zinc-900 border-b border-zinc-700 rounded-t-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Activity className="h-5 w-5 text-blue-500" />
|
||||||
|
<span className="text-zinc-300 text-sm font-semibold">ProxMenux Terminal</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`text-xs ${
|
||||||
|
isConnected
|
||||||
|
? "border-green-500 text-green-500 bg-green-500/10"
|
||||||
|
: "border-red-500 text-red-500 bg-red-500/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-1.5 ${isConnected ? "bg-green-500" : "bg-red-500"}`}></div>
|
||||||
|
{isConnected ? "Connected" : "Disconnected"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleClear}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 gap-2 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Clear</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 gap-2 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Close</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex-1 bg-black rounded-t-md overflow-hidden"
|
className="flex-1 bg-[#0d1117] overflow-auto min-h-0"
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={touchStartRef.current ? undefined : handleTouchStart}
|
||||||
onTouchEnd={handleTouchEnd}
|
onTouchEnd={touchStartRef.current ? handleTouchEnd : undefined}
|
||||||
>
|
>
|
||||||
{!xtermLoaded && (
|
{!xtermLoaded && (
|
||||||
<div className="flex items-center justify-center h-full text-zinc-400">Initializing terminal...</div>
|
<div className="flex items-center justify-center h-full text-zinc-400">Initializing terminal...</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center items-center px-2 py-2 bg-zinc-900 text-sm rounded-b-md">
|
{isMobile && (
|
||||||
<TouchKey label="ESC" onClick={() => handleKeyButton("ESC")} />
|
<div className="px-3 py-2 bg-zinc-900/50 border-t border-zinc-700">
|
||||||
<TouchKey label="TAB" onClick={() => handleKeyButton("TAB")} />
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<TouchKey label="↑" onClick={() => handleKeyButton("UP")} />
|
<span className="text-xs text-zinc-400">Mobile Input</span>
|
||||||
<TouchKey label="↓" onClick={() => handleKeyButton("DOWN")} />
|
{lastKeyPressed && (
|
||||||
<TouchKey label="←" onClick={() => handleKeyButton("LEFT")} />
|
<span className="text-xs text-green-500 bg-green-500/10 px-2 py-0.5 rounded">Sent: {lastKeyPressed}</span>
|
||||||
<TouchKey label="→" onClick={() => handleKeyButton("RIGHT")} />
|
)}
|
||||||
<TouchKey label="ENTER" onClick={() => handleKeyButton("ENTER")} />
|
|
||||||
<TouchKey label="CTRL+C" onClick={() => handleKeyButton("CTRL_C")} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex gap-2">
|
||||||
)
|
<input
|
||||||
}
|
type="text"
|
||||||
|
value={mobileInput}
|
||||||
type TouchKeyProps = {
|
onChange={(e) => setMobileInput(e.target.value)}
|
||||||
label: string
|
onKeyDown={(e) => e.key === "Enter" && handleMobileInputSend()}
|
||||||
onClick: () => void
|
placeholder="Type command..."
|
||||||
}
|
className="flex-1 px-3 py-2 text-sm border border-zinc-600 rounded-md bg-zinc-800 text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={!isConnected}
|
||||||
const TouchKey: React.FC<TouchKeyProps> = ({ label, onClick }) => (
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
onClick={handleMobileInputSend}
|
||||||
onClick={onClick}
|
variant="default"
|
||||||
className="px-3 py-1 rounded bg-zinc-800 hover:bg-zinc-700 active:bg-zinc-600 text-zinc-100 text-xs md:text-sm border border-zinc-700"
|
size="sm"
|
||||||
|
disabled={!isConnected || !mobileInput.trim()}
|
||||||
|
className="px-3 bg-blue-600 hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{label}
|
<Send className="h-4 w-4" />
|
||||||
</button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center items-center px-2 py-2 bg-zinc-900 text-sm rounded-b-md border-t border-zinc-700">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("ESC")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-3 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
ESC
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("TAB")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-3 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
TAB
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("UP")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-2 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("DOWN")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-2 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("LEFT")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-2 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("RIGHT")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-2 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleKeyButton("CTRL_C")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!isConnected}
|
||||||
|
className="h-8 px-3 bg-zinc-800 hover:bg-zinc-700 border-zinc-600 text-zinc-100"
|
||||||
|
>
|
||||||
|
CTRL+C
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ def terminal_websocket(ws):
|
|||||||
stdout=slave_fd,
|
stdout=slave_fd,
|
||||||
stderr=slave_fd,
|
stderr=slave_fd,
|
||||||
preexec_fn=os.setsid,
|
preexec_fn=os.setsid,
|
||||||
|
cwd='/',
|
||||||
env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ')
|
env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user