fix: Proxmox TunnelTab SSH-Copy identisch zu OPNsense (auto-copy + grüne Bestätigung + SSH-Button pro Tunnel)
This commit is contained in:
parent
67a337ed9f
commit
73ff57eaf0
@ -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(() => {
|
||||||
|
if (targetPort === webguiPort) {
|
||||||
window.open(`https://${backendHost}:${data.proxy_port}`, '_blank')
|
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,81 +780,133 @@ 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 && (
|
|
||||||
<div className="flex items-center gap-3 bg-gray-900/60 border border-green-700/40 rounded-lg px-4 py-3">
|
|
||||||
<Terminal className="w-4 h-4 text-green-400 shrink-0" />
|
|
||||||
<code className="flex-1 text-sm font-mono text-green-300">{sshCmd}</code>
|
|
||||||
<button
|
|
||||||
onClick={copySSHCmd}
|
|
||||||
className={`shrink-0 flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-medium transition-all ${
|
|
||||||
copiedCmd
|
|
||||||
? 'bg-green-700 text-white'
|
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{copiedCmd ? '✓ Kopiert' : 'Kopieren'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{customOpen && (
|
{customOpen && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4 flex items-end gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">Ziel-Host</label>
|
||||||
<input type="text" value={customHost} onChange={(e) => setCustomHost(e.target.value)}
|
<input type="text" value={customHost} onChange={(e) => setCustomHost(e.target.value)}
|
||||||
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" />
|
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" />
|
||||||
<span className="text-gray-500">:</span>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">Port</label>
|
||||||
<input type="number" value={customPort} onChange={(e) => setCustomPort(e.target.value)}
|
<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"
|
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"
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleCustom()} />
|
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>
|
||||||
|
<button onClick={handleCustom}
|
||||||
|
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-sm text-white transition-colors">
|
||||||
|
Verbinden
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <div className="text-red-400 text-sm">{error}</div>}
|
{/* SSH auto-kopiert Bestätigung */}
|
||||||
|
{sshCopied && (
|
||||||
|
<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">
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
SSH-Befehl kopiert — in CMD/Terminal einfuegen und ausfuehren
|
||||||
|
</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>}
|
||||||
|
|
||||||
{tunnels.length > 0 && (
|
{/* Aktive Tunnel */}
|
||||||
<>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-300">Aktive Tunnel ({tunnels.length})</h3>
|
<h3 className="text-sm font-medium text-gray-300 mb-2">Aktive Tunnel</h3>
|
||||||
|
{loading ? (
|
||||||
|
<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 (
|
||||||
|
<tr key={tid}>
|
||||||
|
<td className="px-3 py-2.5 text-white font-mono text-xs">{target}</td>
|
||||||
|
<td className="px-3 py-2.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-gray-400 font-mono text-xs">{backendHost}:{proxyPort}</span>
|
||||||
|
{isWebGUI && proxyPort && (
|
||||||
|
<a href={`https://${backendHost}:${proxyPort}`} target="_blank" rel="noopener"
|
||||||
|
className="text-orange-400 hover:text-orange-300" title="WebGUI oeffnen">
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
</a>
|
</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>
|
||||||
<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">
|
<span className="inline-flex items-center gap-1.5 text-xs text-green-400">
|
||||||
<X className="w-4 h-4" />
|
<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>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user