diff --git a/frontend/src/components/fleet/WinBoxButton.tsx b/frontend/src/components/fleet/WinBoxButton.tsx new file mode 100644 index 0000000..edefe4b --- /dev/null +++ b/frontend/src/components/fleet/WinBoxButton.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react' +import { useMutation } from '@tanstack/react-query' +import { Monitor, Copy, X, Loader2 } from 'lucide-react' +import { remoteAccessApi } from '@/lib/api' + +interface WinBoxButtonProps { + tenantId: string + deviceId: string +} + +type State = 'idle' | 'requesting' | 'ready' | 'closing' | 'error' + +export function WinBoxButton({ tenantId, deviceId }: WinBoxButtonProps) { + const [state, setState] = useState('idle') + const [tunnelInfo, setTunnelInfo] = useState<{ + tunnel_id: string + host: string + port: number + winbox_uri: string + } | null>(null) + const [error, setError] = useState(null) + const [copied, setCopied] = useState(false) + + const openMutation = useMutation({ + mutationFn: () => remoteAccessApi.openWinbox(tenantId, deviceId), + onSuccess: (data) => { + setTunnelInfo(data) + setState('ready') + + // Attempt deep link on Windows only + if (navigator.userAgent.includes('Windows')) { + window.open(data.winbox_uri, '_blank') + } + }, + onError: (err: any) => { + setState('error') + setError(err.response?.data?.detail || 'Failed to open tunnel') + }, + }) + + const closeMutation = useMutation({ + mutationFn: () => { + if (!tunnelInfo) throw new Error('No tunnel') + return remoteAccessApi.closeWinbox(tenantId, deviceId, tunnelInfo.tunnel_id) + }, + onSuccess: () => { + setState('idle') + setTunnelInfo(null) + }, + }) + + const copyAddress = async () => { + if (!tunnelInfo) return + const addr = `${tunnelInfo.host}:${tunnelInfo.port}` + try { + await navigator.clipboard.writeText(addr) + } catch { + // Fallback for HTTP + const ta = document.createElement('textarea') + ta.value = addr + document.body.appendChild(ta) + ta.select() + document.execCommand('copy') + document.body.removeChild(ta) + } + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + if (state === 'idle' || state === 'error') { + return ( +
+ + {error &&

{error}

} +
+ ) + } + + if (state === 'ready' && tunnelInfo) { + return ( +
+

WinBox tunnel ready

+

+ Connect to: {tunnelInfo.host}:{tunnelInfo.port} +

+
+ + +
+

+ Tunnel closes after 5 min of inactivity +

+
+ ) + } + + return null +}