// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks" import { createPortal } from "preact/compat" import type { VNode } from "preact" import { runSSHSession, SSHSessionDef } from "../lib/ssh" export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { const [sshSessionDef, setSSHSessionDef] = useState( null ) const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), []) if (sshSessionDef) { const sshSession = ( ) if (sshSessionDef.newWindow) { return {sshSession} } return sshSession } const sshPeers = netMap.peers.filter( (p) => p.tailscaleSSHEnabled && p.online !== false ) if (sshPeers.length == 0) { return } return } type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean } function SSHSession({ def, ipn, onDone, }: { def: SSHSessionDef ipn: IPN onDone: () => void }) { const ref = useRef(null) useEffect(() => { if (ref.current) { runSSHSession(ref.current, def, ipn, { onConnectionProgress: (p) => console.log("Connection progress", p), onConnected() {}, onError: (err) => console.error(err), onDone, }) } }, [ref]) return
} function NoSSHPeers() { return (
None of your machines have{" "} Tailscale SSH {" "}enabled. Give it a try!
) } function SSHForm({ sshPeers, onSubmit, }: { sshPeers: IPNNetMapPeerNode[] onSubmit: (def: SSHFormSessionDef) => void }) { sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name)) const [username, setUsername] = useState("") const [hostname, setHostname] = useState(sshPeers[0].name) return (
{ e.preventDefault() onSubmit({ username, hostname }) }} > setUsername(e.currentTarget.value)} />
{ if (e.altKey) { e.preventDefault() e.stopPropagation() onSubmit({ username, hostname, newWindow: true }) } }} />
) } const NewWindow = ({ children, close, }: { children: VNode close: () => void }) => { const newWindow = useMemo(() => { const newWindow = window.open(undefined, undefined, "width=600,height=400") if (newWindow) { const containerNode = newWindow.document.createElement("div") containerNode.className = "h-screen flex flex-col overflow-hidden" newWindow.document.body.appendChild(containerNode) for (const linkNode of document.querySelectorAll( "head link[rel=stylesheet]" )) { const newLink = document.createElement("link") newLink.rel = "stylesheet" newLink.href = (linkNode as HTMLLinkElement).href newWindow.document.head.appendChild(newLink) } } return newWindow }, []) if (!newWindow) { console.error("Could not open window") return null } newWindow.onbeforeunload = () => { close() } useEffect(() => () => newWindow.close(), []) return createPortal(children, newWindow.document.body.lastChild as Element) }