mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-12-16 09:06:23 +00:00
Update AppImage
This commit is contained in:
@@ -12,6 +12,7 @@ import Hardware from "./hardware"
|
|||||||
import { SystemLogs } from "./system-logs"
|
import { SystemLogs } from "./system-logs"
|
||||||
import { Settings } from "./settings"
|
import { Settings } from "./settings"
|
||||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||||
|
import { HealthStatusModal } from "./health-status-modal"
|
||||||
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
|
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
|
||||||
import { getApiUrl, fetchApi } from "../lib/api-config"
|
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||||
import { TerminalPanel } from "./terminal-panel"
|
import { TerminalPanel } from "./terminal-panel"
|
||||||
@@ -272,7 +273,7 @@ export function ProxmoxDashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-zinc-900 to-black text-white flex flex-col">
|
<div className="min-h-screen bg-background flex flex-col">
|
||||||
<OnboardingCarousel />
|
<OnboardingCarousel />
|
||||||
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
|
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
|
||||||
|
|
||||||
@@ -609,7 +610,7 @@ export function ProxmoxDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6 flex-1 flex flex-col min-h-0">
|
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6 flex-1 flex flex-col">
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={setActiveTab}
|
onValueChange={setActiveTab}
|
||||||
@@ -635,7 +636,7 @@ export function ProxmoxDashboard() {
|
|||||||
<Hardware key={`hardware-${componentKey}`} />
|
<Hardware key={`hardware-${componentKey}`} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="terminal" className="h-[calc(100vh-280px)] flex flex-col overflow-hidden mt-0">
|
<TabsContent value="terminal" className="flex-1 flex flex-col overflow-hidden mt-0 min-h-0">
|
||||||
<TerminalPanel key={`terminal-${componentKey}`} />
|
<TerminalPanel key={`terminal-${componentKey}`} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -647,7 +648,6 @@ export function ProxmoxDashboard() {
|
|||||||
<Settings />
|
<Settings />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground flex-shrink-0">
|
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground flex-shrink-0">
|
||||||
<p className="font-medium mb-2">ProxMenux Monitor v1.0.1</p>
|
<p className="font-medium mb-2">ProxMenux Monitor v1.0.1</p>
|
||||||
@@ -663,5 +663,8 @@ export function ProxmoxDashboard() {
|
|||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HealthStatusModal open={showHealthModal} onOpenChange={setShowHealthModal} getApiUrl={getApiUrl} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
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 } from "lucide-react"
|
import { Activity, Trash2, X, Search, Send, Lightbulb, Terminal, Plus, Split, Grid2X2 } 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"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here
|
import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here
|
||||||
|
|
||||||
type TerminalPanelProps = {
|
type TerminalPanelProps = {
|
||||||
terminals: TerminalInstance[]
|
websocketUrl?: string
|
||||||
onAddTerminal: (terminal: TerminalInstance) => void
|
onClose?: () => void
|
||||||
onRemoveTerminal: (id: string) => void
|
|
||||||
onClearTerminal: (id: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TerminalInstance {
|
interface TerminalInstance {
|
||||||
@@ -120,13 +119,10 @@ const proxmoxCommands = [
|
|||||||
{ cmd: "clear", desc: "Clear terminal screen" },
|
{ cmd: "clear", desc: "Clear terminal screen" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onClose }) => {
|
||||||
terminals,
|
const [terminals, setTerminals] = useState<TerminalInstance[]>([])
|
||||||
onAddTerminal,
|
|
||||||
onRemoveTerminal,
|
|
||||||
onClearTerminal,
|
|
||||||
}) => {
|
|
||||||
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||||
|
const [layout, setLayout] = useState<"single" | "vertical" | "horizontal" | "grid">("single")
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
|
||||||
const [searchModalOpen, setSearchModalOpen] = useState(false)
|
const [searchModalOpen, setSearchModalOpen] = useState(false)
|
||||||
@@ -137,11 +133,11 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
const [searchResults, setSearchResults] = useState<CheatSheetResult[]>([])
|
const [searchResults, setSearchResults] = useState<CheatSheetResult[]>([])
|
||||||
const [useOnline, setUseOnline] = useState(true)
|
const [useOnline, setUseOnline] = useState(true)
|
||||||
|
|
||||||
const terminalRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
||||||
|
|
||||||
const setTerminalRef = useCallback(
|
const setContainerRef = useCallback(
|
||||||
(id: string) => (el: HTMLDivElement | null) => {
|
(id: string) => (el: HTMLDivElement | null) => {
|
||||||
terminalRefs.current[id] = el
|
containerRefs.current[id] = el
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
@@ -155,13 +151,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminals.length === 0) {
|
if (terminals.length === 0) {
|
||||||
onAddTerminal({
|
addNewTerminal()
|
||||||
id: `terminal-${Date.now()}`,
|
|
||||||
title: `Terminal 1`,
|
|
||||||
term: null,
|
|
||||||
ws: null,
|
|
||||||
isConnected: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -234,6 +224,189 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
return () => clearTimeout(debounce)
|
return () => clearTimeout(debounce)
|
||||||
}, [searchQuery])
|
}, [searchQuery])
|
||||||
|
|
||||||
|
const addNewTerminal = () => {
|
||||||
|
if (terminals.length >= 4) return
|
||||||
|
|
||||||
|
const newId = `terminal-${Date.now()}`
|
||||||
|
// containerRefs.current[newId] = useRef<HTMLDivElement>(null) // No longer needed
|
||||||
|
|
||||||
|
setTerminals((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: newId,
|
||||||
|
title: `Terminal ${prev.length + 1}`,
|
||||||
|
term: null,
|
||||||
|
ws: null,
|
||||||
|
isConnected: false,
|
||||||
|
// containerRef: containerRefs.current[newId], // No longer needed
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setActiveTerminalId(newId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeTerminal = (id: string) => {
|
||||||
|
const terminal = terminals.find((t) => t.id === id)
|
||||||
|
if (terminal) {
|
||||||
|
if (terminal.ws) {
|
||||||
|
terminal.ws.close()
|
||||||
|
}
|
||||||
|
if (terminal.term) {
|
||||||
|
terminal.term.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTerminals((prev) => {
|
||||||
|
const filtered = prev.filter((t) => t.id !== id)
|
||||||
|
if (filtered.length > 0 && activeTerminalId === id) {
|
||||||
|
setActiveTerminalId(filtered[0].id)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
delete containerRefs.current[id] // Clean up the ref
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
terminals.forEach((terminal) => {
|
||||||
|
const container = containerRefs.current[terminal.id]
|
||||||
|
if (!terminal.term && container) {
|
||||||
|
initializeTerminal(terminal, container)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [terminals, isMobile])
|
||||||
|
|
||||||
|
const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => {
|
||||||
|
const [Terminal, FitAddon] = await Promise.all([
|
||||||
|
import("xterm").then((mod) => mod.Terminal),
|
||||||
|
import("xterm-addon-fit").then((mod) => mod.FitAddon),
|
||||||
|
import("xterm/css/xterm.css"),
|
||||||
|
]).then(([Terminal, FitAddon]) => [Terminal, FitAddon])
|
||||||
|
|
||||||
|
const term = new Terminal({
|
||||||
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
|
||||||
|
fontSize: isMobile ? 11 : 13,
|
||||||
|
cursorBlink: true,
|
||||||
|
scrollback: 2000,
|
||||||
|
disableStdin: false,
|
||||||
|
theme: {
|
||||||
|
background: "#000000",
|
||||||
|
foreground: "#ffffff",
|
||||||
|
cursor: "#ffffff",
|
||||||
|
cursorAccent: "#000000",
|
||||||
|
black: "#2e3436",
|
||||||
|
red: "#cc0000",
|
||||||
|
green: "#4e9a06",
|
||||||
|
yellow: "#c4a000",
|
||||||
|
blue: "#3465a4",
|
||||||
|
magenta: "#75507b",
|
||||||
|
cyan: "#06989a",
|
||||||
|
white: "#d3d7cf",
|
||||||
|
brightBlack: "#555753",
|
||||||
|
brightRed: "#ef2929",
|
||||||
|
brightGreen: "#8ae234",
|
||||||
|
brightYellow: "#fce94f",
|
||||||
|
brightBlue: "#729fcf",
|
||||||
|
brightMagenta: "#ad7fa8",
|
||||||
|
brightCyan: "#34e2e2",
|
||||||
|
brightWhite: "#eeeeec",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
term.loadAddon(fitAddon)
|
||||||
|
|
||||||
|
term.open(container)
|
||||||
|
|
||||||
|
const performResize = () => {
|
||||||
|
// Ensure xterm viewport has no extra padding
|
||||||
|
const xtermViewport = container.querySelector(".xterm-viewport") as HTMLElement
|
||||||
|
const xtermScreen = container.querySelector(".xterm-screen") as HTMLElement
|
||||||
|
if (xtermViewport) xtermViewport.style.padding = "0"
|
||||||
|
if (xtermScreen) xtermScreen.style.padding = "0"
|
||||||
|
|
||||||
|
// Get actual container dimensions
|
||||||
|
const containerRect = container.getBoundingClientRect()
|
||||||
|
console.log(`[v0] Container dimensions: ${containerRect.width}x${containerRect.height}`)
|
||||||
|
|
||||||
|
// Only resize if container has valid dimensions
|
||||||
|
if (containerRect.width > 0 && containerRect.height > 0) {
|
||||||
|
fitAddon.fit()
|
||||||
|
const cols = term.cols
|
||||||
|
const rows = term.rows
|
||||||
|
console.log(`[v0] Terminal fitted to: ${cols}x${rows}`)
|
||||||
|
|
||||||
|
// Send resize to backend via HTTP
|
||||||
|
const apiUrl = getApiUrl()
|
||||||
|
fetch(`${apiUrl}/api/terminal/${terminal.id}/resize`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ cols, rows }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.log(`[v0] Backend PTY resized to: ${data.cols}x${data.rows}`)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`[v0] Error resizing backend PTY:`, err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log(`[v0] Container not ready yet, dimensions: ${containerRect.width}x${containerRect.height}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => performResize(), 150)
|
||||||
|
setTimeout(() => performResize(), 400)
|
||||||
|
setTimeout(() => performResize(), 800)
|
||||||
|
|
||||||
|
const wsUrl = websocketUrl || getWebSocketUrl()
|
||||||
|
const ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws } : t)))
|
||||||
|
term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m")
|
||||||
|
|
||||||
|
setTimeout(() => performResize(), 250)
|
||||||
|
setTimeout(() => performResize(), 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
term.write(event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error("[v0] TerminalPanel: WebSocket error:", error)
|
||||||
|
setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t)))
|
||||||
|
term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t)))
|
||||||
|
term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
term.onData((data) => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
try {
|
||||||
|
performResize()
|
||||||
|
} catch {
|
||||||
|
// Ignore resize errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize)
|
||||||
|
ws.close()
|
||||||
|
term.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyButton = (key: string) => {
|
const handleKeyButton = (key: string) => {
|
||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||||
if (!activeTerminal || !activeTerminal.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return
|
if (!activeTerminal || !activeTerminal.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return
|
||||||
@@ -270,12 +443,19 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||||
|
if (activeTerminal?.term) {
|
||||||
|
activeTerminal.term.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
terminals.forEach((terminal) => {
|
terminals.forEach((terminal) => {
|
||||||
if (terminal.ws) terminal.ws.close()
|
if (terminal.ws) terminal.ws.close()
|
||||||
if (terminal.term) terminal.term.dispose()
|
if (terminal.term) terminal.term.dispose()
|
||||||
})
|
})
|
||||||
onRemoveTerminal(activeTerminalId)
|
onClose?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendToActiveTerminal = (command: string) => {
|
const sendToActiveTerminal = (command: string) => {
|
||||||
@@ -307,76 +487,16 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
const getLayoutClass = () => {
|
const getLayoutClass = () => {
|
||||||
const count = terminals.length
|
const count = terminals.length
|
||||||
if (isMobile || count === 1) return "grid grid-cols-1"
|
if (isMobile || count === 1) return "grid grid-cols-1"
|
||||||
if (count === 2) return "grid grid-cols-2"
|
if (layout === "vertical" || count === 2) return "grid grid-cols-2"
|
||||||
if (count >= 3) return "grid grid-cols-2 grid-rows-2"
|
if (layout === "horizontal") return "grid grid-rows-2"
|
||||||
|
if (layout === "grid" || count >= 3) return "grid grid-cols-2 grid-rows-2"
|
||||||
return "grid grid-cols-1"
|
return "grid grid-cols-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||||
|
|
||||||
const renderTerminals = () => {
|
|
||||||
if (terminals.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
<>
|
||||||
<p>No terminals open. Click "New" to create one.</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (terminals.length === 1 || isMobile) {
|
|
||||||
// Single terminal mode - fill entire container
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex flex-col bg-black">
|
|
||||||
<div className="flex items-center justify-between border-b border-border px-4 py-2 bg-muted/50 flex-shrink-0">
|
|
||||||
<span className="text-sm font-medium">Terminal {terminals[0].id}</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => onRemoveTerminal(terminals[0].id)} className="h-6 w-6 p-0">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div ref={(el) => (terminalRefs.current[terminals[0].id] = el)} className="flex-1 w-full bg-black" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (terminals.length === 2) {
|
|
||||||
// Split view - two terminals side by side
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex gap-2">
|
|
||||||
{terminals.map((terminal) => (
|
|
||||||
<div key={terminal.id} className="flex-1 flex flex-col border border-border rounded bg-black min-w-0">
|
|
||||||
<div className="flex items-center justify-between border-b border-border px-4 py-2 bg-muted/50 flex-shrink-0">
|
|
||||||
<span className="text-sm font-medium">Terminal {terminal.id}</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => onRemoveTerminal(terminal.id)} className="h-6 w-6 p-0">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div ref={(el) => (terminalRefs.current[terminal.id] = el)} className="flex-1 w-full bg-black" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grid view for 3+ terminals
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full grid grid-cols-2 gap-2">
|
|
||||||
{terminals.map((terminal) => (
|
|
||||||
<div key={terminal.id} className="flex flex-col border border-border rounded bg-black min-h-0">
|
|
||||||
<div className="flex items-center justify-between border-b border-border px-4 py-2 bg-muted/50 flex-shrink-0">
|
|
||||||
<span className="text-sm font-medium">Terminal {terminal.id}</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => onRemoveTerminal(terminal.id)} className="h-6 w-6 p-0">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div ref={(el) => (terminalRefs.current[terminal.id] = el)} className="flex-1 w-full bg-black" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
:global(.xterm .xterm-viewport) {
|
:global(.xterm .xterm-viewport) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
@@ -387,30 +507,51 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div className="h-full flex flex-col bg-zinc-900 rounded-md overflow-hidden">
|
<div className="h-full flex flex-col bg-zinc-900 rounded-md overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800 flex-shrink-0">
|
<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" />
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${activeTerminal?.isConnected ? "bg-green-500" : "bg-red-500"}`}
|
className={`w-2 h-2 rounded-full ${activeTerminal?.isConnected ? "bg-green-500" : "bg-red-500"}`}
|
||||||
title={activeTerminal?.isConnected ? "Connected" : "Disconnected"}
|
title={activeTerminal?.isConnected ? "Connected" : "Disconnected"}
|
||||||
></div>
|
></div>
|
||||||
<span className="text-xs text-zinc-500">{terminals.length} terminals</span>
|
<span className="text-xs text-zinc-500">{terminals.length} / 4 terminals</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{!isMobile && terminals.length > 1 && (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() => setLayout("vertical")}
|
||||||
onAddTerminal({
|
|
||||||
id: `terminal-${Date.now()}`,
|
|
||||||
title: `Terminal ${terminals.length + 1}`,
|
|
||||||
term: null,
|
|
||||||
ws: null,
|
|
||||||
isConnected: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 gap-2 bg-green-600 hover:bg-green-700 border-green-500 text-white"
|
className={`h-8 px-2 ${layout === "vertical" ? "bg-blue-500/20 border-blue-500" : ""}`}
|
||||||
|
>
|
||||||
|
<Split className="h-4 w-4 rotate-90" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setLayout("horizontal")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={`h-8 px-2 ${layout === "horizontal" ? "bg-blue-500/20 border-blue-500" : ""}`}
|
||||||
|
>
|
||||||
|
<Split className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setLayout("grid")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={`h-8 px-2 ${layout === "grid" ? "bg-blue-500/20 border-blue-500" : ""}`}
|
||||||
|
>
|
||||||
|
<Grid2X2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={addNewTerminal}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={terminals.length >= 4}
|
||||||
|
className="h-8 gap-2 bg-green-600 hover:bg-green-700 border-green-500 text-white disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">New</span>
|
<span className="hidden sm:inline">New</span>
|
||||||
@@ -426,7 +567,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
<span className="hidden sm:inline">Search</span>
|
<span className="hidden sm:inline">Search</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onClearTerminal(activeTerminalId)}
|
onClick={handleClear}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!activeTerminal?.isConnected}
|
disabled={!activeTerminal?.isConnected}
|
||||||
@@ -447,7 +588,62 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-[calc(100%-60px)] overflow-hidden">{renderTerminals()}</div>
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{isMobile ? (
|
||||||
|
<Tabs value={activeTerminalId} onValueChange={setActiveTerminalId} className="h-full flex flex-col">
|
||||||
|
<TabsList className="bg-zinc-900 border-b border-zinc-800 rounded-none justify-start overflow-x-auto flex-shrink-0">
|
||||||
|
{terminals.map((terminal) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={terminal.id}
|
||||||
|
value={terminal.id}
|
||||||
|
className="data-[state=active]:bg-blue-500/20 data-[state=active]:border-b-2 data-[state=active]:border-blue-500 rounded-none"
|
||||||
|
>
|
||||||
|
{terminal.title}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{terminals.map((terminal) => (
|
||||||
|
<TabsContent
|
||||||
|
key={terminal.id}
|
||||||
|
value={terminal.id}
|
||||||
|
className="flex-1 m-0 data-[state=active]:flex data-[state=inactive]:hidden"
|
||||||
|
>
|
||||||
|
<div ref={setContainerRef(terminal.id)} className="w-full h-full bg-black" />
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
) : (
|
||||||
|
<div className={`${getLayoutClass()} gap-2 h-full p-2`}>
|
||||||
|
{terminals.map((terminal) => (
|
||||||
|
<div
|
||||||
|
key={terminal.id}
|
||||||
|
className={`relative bg-black rounded flex flex-col overflow-hidden border ${
|
||||||
|
terminal.id === activeTerminalId ? "border-blue-500" : "border-zinc-700"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTerminalId(terminal.id)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-2 py-1 bg-zinc-800 border-b border-zinc-700">
|
||||||
|
<span className="text-xs font-medium text-white">{terminal.title}</span>
|
||||||
|
{terminals.length > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
closeTerminal(terminal.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div ref={setContainerRef(terminal.id)} className="flex-1 w-full overflow-hidden bg-black" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="flex flex-wrap gap-2 justify-center items-center px-2 bg-zinc-900 text-sm rounded-b-md border-t border-zinc-700 py-1.5">
|
<div className="flex flex-wrap gap-2 justify-center items-center px-2 bg-zinc-900 text-sm rounded-b-md border-t border-zinc-700 py-1.5">
|
||||||
@@ -654,6 +850,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user