fix: Proxmox TunnelTab SSH-Copy identisch zu OPNsense (auto-copy + grüne Bestätigung + SSH-Button pro Tunnel)

This commit is contained in:
cynfo3000 2026-03-07 00:24:29 +01:00
parent 67a337ed9f
commit 73ff57eaf0

View File

@ -6,7 +6,7 @@ import TerminalModal from './TerminalModal'
import { import {
X, Cpu, MemoryStick, HardDrive, Clock, Network, Server, Monitor, X, Cpu, MemoryStick, HardDrive, Clock, Network, Server, Monitor,
Container, Database, FolderTree, Settings, Download, Trash2, Copy, Check, Container, Database, FolderTree, Settings, Download, Trash2, Copy, Check,
MonitorSmartphone, Terminal, Cable, ExternalLink, MonitorSmartphone, Terminal, Cable, ExternalLink, Plus, Unplug,
} from 'lucide-react' } from 'lucide-react'
const buildTabs = (hasPBS) => [ const buildTabs = (hasPBS) => [
@ -679,8 +679,8 @@ function TunnelTab({ agentId, agent, webguiPort = 8006 }) {
const [customHost, setCustomHost] = useState('127.0.0.1') const [customHost, setCustomHost] = useState('127.0.0.1')
const [customPort, setCustomPort] = useState('') const [customPort, setCustomPort] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [sshCopied, setSshCopied] = useState(null)
const [terminalOpen, setTerminalOpen] = useState(false) const [terminalOpen, setTerminalOpen] = useState(false)
const [copiedCmd, setCopiedCmd] = useState(false)
const backendHost = useSettingsStore.getState().getBackendHost() const backendHost = useSettingsStore.getState().getBackendHost()
@ -709,9 +709,16 @@ function TunnelTab({ agentId, agent, webguiPort = 8006 }) {
const resp = await api.openTunnel(agentId, targetHost, targetPort) const resp = await api.openTunnel(agentId, targetHost, targetPort)
const data = resp?.data || resp const data = resp?.data || resp
loadTunnels() loadTunnels()
if (targetPort === webguiPort && data?.proxy_port) { if (data?.proxy_port) {
setTimeout(() => { setTimeout(() => {
window.open(`https://${backendHost}:${data.proxy_port}`, '_blank') if (targetPort === webguiPort) {
window.open(`https://${backendHost}:${data.proxy_port}`, '_blank')
} else if (targetPort === 22) {
const cmd = `ssh root@${backendHost} -p ${data.proxy_port}`
navigator.clipboard.writeText(cmd).catch(() => {})
setSshCopied(data.proxy_port)
setTimeout(() => setSshCopied(null), 4000)
}
}, 1000) }, 1000)
} }
return data return data
@ -739,23 +746,13 @@ function TunnelTab({ agentId, agent, webguiPort = 8006 }) {
setCustomPort('') setCustomPort('')
} }
const sshTunnel = tunnels.find(t => t.target_port === 22)
const sshCmd = sshTunnel ? `ssh root@${backendHost} -p ${sshTunnel.proxy_port}` : null
const copySSHCmd = () => {
if (sshCmd) {
navigator.clipboard.writeText(sshCmd)
setCopiedCmd(true)
setTimeout(() => setCopiedCmd(false), 2000)
}
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{terminalOpen && ( {terminalOpen && (
<TerminalModal agentId={agentId} agentName={agent?.name} onClose={() => setTerminalOpen(false)} /> <TerminalModal agentId={agentId} agentName={agent?.name} onClose={() => setTerminalOpen(false)} />
)} )}
{/* Schnellzugriff */}
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center gap-3 flex-wrap">
<span className="text-sm text-gray-400">Schnellzugriff:</span> <span className="text-sm text-gray-400">Schnellzugriff:</span>
<button <button
@ -769,10 +766,10 @@ function TunnelTab({ agentId, agent, webguiPort = 8006 }) {
<button <button
onClick={() => openTunnel('127.0.0.1', 22, 'ssh')} onClick={() => openTunnel('127.0.0.1', 22, 'ssh')}
disabled={creating === 'ssh'} disabled={creating === 'ssh'}
className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 disabled:opacity-50 rounded text-sm text-white transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:opacity-50 rounded text-sm text-white transition-colors"
> >
<Terminal className="w-4 h-4" /> <Terminal className="w-4 h-4" />
{creating === 'ssh' ? 'Verbinde...' : 'SSH Tunnel'} {creating === 'ssh' ? 'Verbinde...' : 'SSH (22)'}
</button> </button>
<button <button
onClick={() => setTerminalOpen(true)} onClick={() => setTerminalOpen(true)}
@ -783,80 +780,132 @@ function TunnelTab({ agentId, agent, webguiPort = 8006 }) {
</button> </button>
<button <button
onClick={() => setCustomOpen(!customOpen)} onClick={() => setCustomOpen(!customOpen)}
className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm text-white transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm text-gray-300 transition-colors"
> >
<Cable className="w-4 h-4" /> Benutzerdefiniert <Plus className="w-4 h-4" />
Benutzerdefiniert
</button> </button>
</div> </div>
{/* SSH-Copy-Karte wenn SSH-Tunnel aktiv */} {/* Custom Form */}
{sshTunnel && ( {customOpen && (
<div className="flex items-center gap-3 bg-gray-900/60 border border-green-700/40 rounded-lg px-4 py-3"> <div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4 flex items-end gap-3">
<Terminal className="w-4 h-4 text-green-400 shrink-0" /> <div>
<code className="flex-1 text-sm font-mono text-green-300">{sshCmd}</code> <label className="block text-xs text-gray-400 mb-1">Ziel-Host</label>
<button <input type="text" value={customHost} onChange={(e) => setCustomHost(e.target.value)}
onClick={copySSHCmd} className="bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white w-40 focus:outline-none focus:border-orange-500" />
className={`shrink-0 flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-medium transition-all ${ </div>
copiedCmd <div>
? 'bg-green-700 text-white' <label className="block text-xs text-gray-400 mb-1">Port</label>
: 'bg-gray-700 hover:bg-gray-600 text-gray-300' <input type="number" value={customPort} onChange={(e) => setCustomPort(e.target.value)}
}`} placeholder="z.B. 8080"
> className="bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white w-28 focus:outline-none focus:border-orange-500"
{copiedCmd ? '✓ Kopiert' : 'Kopieren'} onKeyDown={(e) => e.key === 'Enter' && handleCustom()} />
</div>
<button onClick={handleCustom}
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-sm text-white transition-colors">
Verbinden
</button> </button>
</div> </div>
)} )}
{customOpen && ( {/* SSH auto-kopiert Bestätigung */}
<div className="flex items-center gap-2 text-sm"> {sshCopied && (
<input type="text" value={customHost} onChange={(e) => setCustomHost(e.target.value)} <div className="text-sm text-green-400 bg-green-900/20 rounded px-3 py-2 border border-green-800/50 flex items-center gap-2">
className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white w-36 focus:outline-none focus:border-orange-500" placeholder="Host" /> <Terminal className="w-4 h-4" />
<span className="text-gray-500">:</span> SSH-Befehl kopiert in CMD/Terminal einfuegen und ausfuehren
<input type="number" value={customPort} onChange={(e) => setCustomPort(e.target.value)}
className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white w-20 focus:outline-none focus:border-orange-500" placeholder="Port"
onKeyDown={(e) => e.key === 'Enter' && handleCustom()} />
<button onClick={handleCustom} className="px-3 py-1 bg-orange-600 hover:bg-orange-500 rounded text-white transition-colors">Verbinden</button>
</div> </div>
)} )}
{error && <div className="text-sm text-red-400 bg-red-900/20 rounded px-3 py-2 border border-red-800/50">{error}</div>}
{error && <div className="text-red-400 text-sm">{error}</div>} {/* Aktive Tunnel */}
<div>
{tunnels.length > 0 && ( <h3 className="text-sm font-medium text-gray-300 mb-2">Aktive Tunnel</h3>
<> {loading ? (
<h3 className="text-sm font-medium text-gray-300">Aktive Tunnel ({tunnels.length})</h3> <div className="text-gray-500 text-sm">Laden...</div>
) : tunnels.length === 0 ? (
<div className="text-gray-500 bg-gray-800/30 rounded-lg border border-gray-800 px-4 py-6 text-center text-sm">
Keine aktiven Tunnel
</div>
) : (
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden"> <div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="text-left text-gray-500 text-xs border-b border-gray-700"> <tr className="text-left text-gray-500 text-xs border-b border-gray-700">
<th className="px-3 py-2">Ziel</th> <th className="px-3 py-2">Ziel</th>
<th className="px-3 py-2">Proxy Port</th> <th className="px-3 py-2">Lokaler Zugang</th>
<th className="px-3 py-2">Zugriff</th> <th className="px-3 py-2">Status</th>
<th className="px-3 py-2 w-10"></th> <th className="px-3 py-2">Erstellt</th>
<th className="px-3 py-2 text-right">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-700/50"> <tbody className="divide-y divide-gray-700/50">
{tunnels.map((t) => ( {tunnels.map((t) => {
<tr key={t.tunnel_id || t.id}> const tid = t.id || t.tunnel_id
<td className="px-3 py-2 text-white">{t.target_host}:{t.target_port}</td> const proxyPort = t.proxy_port || t.local_port
<td className="px-3 py-2 text-orange-400 font-mono">{t.proxy_port}</td> const target = t.target_host && t.target_port ? `${t.target_host}:${t.target_port}` : '—'
<td className="px-3 py-2"> const isWebGUI = t.target_port === webguiPort || t.target_port === String(webguiPort)
<a href={`https://${backendHost}:${t.proxy_port}`} target="_blank" rel="noreferrer" const isSSH = t.target_port === 22 || t.target_port === '22'
className="text-blue-400 hover:text-blue-300 flex items-center gap-1">
<ExternalLink className="w-3 h-3" /> Oeffnen return (
</a> <tr key={tid}>
</td> <td className="px-3 py-2.5 text-white font-mono text-xs">{target}</td>
<td className="px-3 py-2"> <td className="px-3 py-2.5">
<button onClick={() => closeTunnel(t.tunnel_id || t.id)} className="text-gray-500 hover:text-red-400"> <div className="flex items-center gap-2">
<X className="w-4 h-4" /> <span className="text-gray-400 font-mono text-xs">{backendHost}:{proxyPort}</span>
</button> {isWebGUI && proxyPort && (
</td> <a href={`https://${backendHost}:${proxyPort}`} target="_blank" rel="noopener"
</tr> className="text-orange-400 hover:text-orange-300" title="WebGUI oeffnen">
))} <ExternalLink className="w-3.5 h-3.5" />
</a>
)}
{isSSH && proxyPort && (
<button
onClick={() => {
const cmd = `ssh root@${backendHost} -p ${proxyPort}`
navigator.clipboard.writeText(cmd).catch(() => {})
setSshCopied(proxyPort)
setTimeout(() => setSshCopied(null), 3000)
}}
className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded transition-colors ${
sshCopied === proxyPort
? 'bg-green-900/30 text-green-400 border border-green-700/50'
: 'bg-blue-900/30 text-blue-400 border border-blue-700/50 hover:bg-blue-900/50'
}`}
>
<Terminal className="w-3 h-3" />
{sshCopied === proxyPort ? 'Kopiert!' : 'SSH kopieren'}
</button>
)}
</div>
</td>
<td className="px-3 py-2.5">
<span className="inline-flex items-center gap-1.5 text-xs text-green-400">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
aktiv
</span>
</td>
<td className="px-3 py-2.5 text-gray-500 text-xs">
{t.created_at ? new Date(t.created_at).toLocaleString('de-DE') : '—'}
</td>
<td className="px-3 py-2.5 text-right">
<button onClick={() => closeTunnel(tid)}
className="text-red-400 hover:text-red-300 p-1 rounded hover:bg-red-900/30 transition-colors" title="Tunnel schliessen">
<Unplug className="w-4 h-4" />
</button>
</td>
</tr>
)
})}
</tbody> </tbody>
</table> </table>
</div> </div>
</> )}
)} </div>
<div className="text-xs text-gray-600">
Tunnel werden ueber den RMM-Backend-Server ({backendHost}) geroutet. WebGUI-Tunnel oeffnen automatisch ein neues Browserfenster.
</div>
</div> </div>
) )
} }