104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
|
|
import { useEffect, useRef, useState } from 'react'
|
||
|
|
import Guacamole from 'guacamole-common-js'
|
||
|
|
import { api, getToken, type Integration } from '../lib/api'
|
||
|
|
|
||
|
|
const TEXT_SECONDARY = '#7A7D85'
|
||
|
|
|
||
|
|
export default function RemoteDesktop() {
|
||
|
|
const [hosts, setHosts] = useState<Integration[]>([])
|
||
|
|
const [hostId, setHostId] = useState<number | null>(null)
|
||
|
|
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'error'>('idle')
|
||
|
|
const [errorMessage, setErrorMessage] = useState('')
|
||
|
|
const displayRef = useRef<HTMLDivElement>(null)
|
||
|
|
const clientRef = useRef<any>(null)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
api.listIntegrations().then(({ integrations }) => {
|
||
|
|
setHosts(integrations.filter((i) => i.type === 'remote_desktop'))
|
||
|
|
})
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
return () => {
|
||
|
|
clientRef.current?.disconnect()
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
function connect(id: number) {
|
||
|
|
clientRef.current?.disconnect()
|
||
|
|
if (displayRef.current) displayRef.current.innerHTML = ''
|
||
|
|
setStatus('connecting')
|
||
|
|
setErrorMessage('')
|
||
|
|
|
||
|
|
const token = getToken()
|
||
|
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||
|
|
// Guacamole.WebSocketTunnel appends its own "?<data>" query string on connect(),
|
||
|
|
// so the tunnel URL itself must not already contain one.
|
||
|
|
const tunnel = new Guacamole.WebSocketTunnel(`${proto}://${window.location.host}/api/guacamole`)
|
||
|
|
const client = new Guacamole.Client(tunnel)
|
||
|
|
clientRef.current = client
|
||
|
|
|
||
|
|
client.onerror = (err: { message?: string }) => {
|
||
|
|
setStatus('error')
|
||
|
|
setErrorMessage(err?.message ?? 'Connection failed')
|
||
|
|
}
|
||
|
|
client.onstatechange = (state: number) => {
|
||
|
|
if (state === 3) setStatus('connected')
|
||
|
|
}
|
||
|
|
|
||
|
|
const display = client.getDisplay().getElement()
|
||
|
|
displayRef.current?.appendChild(display)
|
||
|
|
|
||
|
|
client.connect(`token=${encodeURIComponent(token ?? '')}&integrationId=${id}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleSelect(id: number) {
|
||
|
|
setHostId(id)
|
||
|
|
connect(id)
|
||
|
|
}
|
||
|
|
|
||
|
|
const host = hosts.find((h) => h.id === hostId)
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-full gap-4">
|
||
|
|
<div className="w-56 shrink-0 overflow-y-auto rounded-lg border border-white/10 bg-white/5 p-3">
|
||
|
|
<p className="mb-2 text-xs font-medium uppercase tracking-wide" style={{ color: TEXT_SECONDARY }}>
|
||
|
|
Remote Desktops
|
||
|
|
</p>
|
||
|
|
{hosts.length === 0 && (
|
||
|
|
<p className="text-xs" style={{ color: TEXT_SECONDARY }}>
|
||
|
|
No remote desktop integrations configured. Add one in Settings → Integrations.
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{hosts.map((h) => (
|
||
|
|
<button
|
||
|
|
key={h.id}
|
||
|
|
onClick={() => handleSelect(h.id)}
|
||
|
|
className="mb-1 w-full rounded-md px-2 py-1.5 text-left text-sm transition"
|
||
|
|
style={{
|
||
|
|
background: hostId === h.id ? 'rgba(200,164,52,0.15)' : 'transparent',
|
||
|
|
color: hostId === h.id ? '#C8A434' : '#E8E6E0',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{h.name}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex flex-1 flex-col overflow-hidden rounded-lg border border-white/10 bg-black">
|
||
|
|
<div className="flex items-center justify-between border-b border-white/10 px-3 py-2">
|
||
|
|
<p className="text-sm" style={{ color: TEXT_SECONDARY }}>
|
||
|
|
{host ? host.name : 'Select a remote desktop'}
|
||
|
|
</p>
|
||
|
|
<p className="text-xs" style={{ color: TEXT_SECONDARY }}>
|
||
|
|
{status === 'connecting' && 'Connecting…'}
|
||
|
|
{status === 'connected' && 'Connected'}
|
||
|
|
{status === 'error' && `Error: ${errorMessage}`}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div ref={displayRef} className="flex-1 overflow-auto" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|