cmd/tsconnect: temporarily switch to xterm.js fork that handles popup windows

Allows other work to be unblocked while xtermjs/xterm.js#4069 is worked
through.

To enable testing the popup window handling, the standalone app allows
opening of SSH sessions in new windows by holding down the alt key
while pressing the SSH button.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
This commit is contained in:
Mihai Parparita 2022-09-07 18:23:37 -07:00 committed by Mihai Parparita
parent 2400ba28b1
commit 01e6565e8a
4 changed files with 74 additions and 24 deletions

View File

@ -10,7 +10,7 @@
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"tailwindcss": "^3.1.6", "tailwindcss": "^3.1.6",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"xterm": "^4.18.0", "xterm": "mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67",
"xterm-addon-fit": "^0.5.0" "xterm-addon-fit": "^0.5.0"
}, },
"scripts": { "scripts": {

View File

@ -2,16 +2,24 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
import { useState, useCallback } from "preact/hooks" 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" import { runSSHSession, SSHSessionDef } from "../lib/ssh"
export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
const [sshSessionDef, setSSHSessionDef] = useState<SSHSessionDef | null>(null) const [sshSessionDef, setSSHSessionDef] = useState<SSHFormSessionDef | null>(
null
)
const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), []) const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), [])
if (sshSessionDef) { if (sshSessionDef) {
return ( const sshSession = (
<SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} /> <SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} />
) )
if (sshSessionDef.newWindow) {
return <NewWindow close={clearSSHSessionDef}>{sshSession}</NewWindow>
}
return sshSession
} }
const sshPeers = netMap.peers.filter( const sshPeers = netMap.peers.filter(
(p) => p.tailscaleSSHEnabled && p.online !== false (p) => p.tailscaleSSHEnabled && p.online !== false
@ -24,6 +32,8 @@ export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
return <SSHForm sshPeers={sshPeers} onSubmit={setSSHSessionDef} /> return <SSHForm sshPeers={sshPeers} onSubmit={setSSHSessionDef} />
} }
type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean }
function SSHSession({ function SSHSession({
def, def,
ipn, ipn,
@ -33,20 +43,14 @@ function SSHSession({
ipn: IPN ipn: IPN
onDone: () => void onDone: () => void
}) { }) {
return ( const ref = useRef<HTMLDivElement>(null)
<div useEffect(() => {
class="flex-grow bg-black p-2 overflow-hidden" if (ref.current) {
ref={(node) => { runSSHSession(ref.current, def, ipn, onDone)
if (node) { }
// Run the SSH session aysnchronously, so that the React render }, [ref])
// loop is complete (otherwise the SSH form may still be visible,
// which affects the size of the terminal, leading to a spurious return <div class="flex-grow bg-black p-2 overflow-hidden" ref={ref} />
// initial resize).
setTimeout(() => runSSHSession(node, def, ipn, onDone), 0)
}
}}
/>
)
} }
function NoSSHPeers() { function NoSSHPeers() {
@ -66,7 +70,7 @@ function SSHForm({
onSubmit, onSubmit,
}: { }: {
sshPeers: IPNNetMapPeerNode[] sshPeers: IPNNetMapPeerNode[]
onSubmit: (def: SSHSessionDef) => void onSubmit: (def: SSHFormSessionDef) => void
}) { }) {
sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name)) sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name))
const [username, setUsername] = useState("") const [username, setUsername] = useState("")
@ -99,7 +103,51 @@ function SSHForm({
type="submit" type="submit"
class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600" class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
value="SSH" value="SSH"
onClick={(e) => {
if (e.altKey) {
e.preventDefault()
e.stopPropagation()
onSubmit({ username, hostname, newWindow: true })
}
}}
/> />
</form> </form>
) )
} }
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)
}

View File

@ -54,7 +54,10 @@ export function runSSHSession(
}) })
// Make terminal and SSH session track the size of the containing DOM node. // Make terminal and SSH session track the size of the containing DOM node.
resizeObserver = new ResizeObserver(() => fitAddon.fit()) resizeObserver =
new termContainerNode.ownerDocument.defaultView!.ResizeObserver(() =>
fitAddon.fit()
)
resizeObserver.observe(termContainerNode) resizeObserver.observe(termContainerNode)
term.onResize(({ rows, cols }) => sshSession.resize(rows, cols)) term.onResize(({ rows, cols }) => sshSession.resize(rows, cols))

View File

@ -644,10 +644,9 @@ xterm-addon-fit@^0.5.0:
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ== integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
xterm@^4.18.0: xterm@mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67:
version "4.18.0" version "4.19.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1" resolved "https://codeload.github.com/mihaip/xterm.js/tar.gz/95100e3c870b59348bb4fa5504bab9f9317bac67"
integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==
y18n@^4.0.0: y18n@^4.0.0:
version "4.0.3" version "4.0.3"