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",
"tailwindcss": "^3.1.6",
"typescript": "^4.7.4",
"xterm": "^4.18.0",
"xterm": "mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67",
"xterm-addon-fit": "^0.5.0"
},
"scripts": {

View File

@ -2,16 +2,24 @@
// Use of this source code is governed by a BSD-style
// 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"
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), [])
if (sshSessionDef) {
return (
const sshSession = (
<SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} />
)
if (sshSessionDef.newWindow) {
return <NewWindow close={clearSSHSessionDef}>{sshSession}</NewWindow>
}
return sshSession
}
const sshPeers = netMap.peers.filter(
(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} />
}
type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean }
function SSHSession({
def,
ipn,
@ -33,20 +43,14 @@ function SSHSession({
ipn: IPN
onDone: () => void
}) {
return (
<div
class="flex-grow bg-black p-2 overflow-hidden"
ref={(node) => {
if (node) {
// Run the SSH session aysnchronously, so that the React render
// loop is complete (otherwise the SSH form may still be visible,
// which affects the size of the terminal, leading to a spurious
// initial resize).
setTimeout(() => runSSHSession(node, def, ipn, onDone), 0)
}
}}
/>
)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
runSSHSession(ref.current, def, ipn, onDone)
}
}, [ref])
return <div class="flex-grow bg-black p-2 overflow-hidden" ref={ref} />
}
function NoSSHPeers() {
@ -66,7 +70,7 @@ function SSHForm({
onSubmit,
}: {
sshPeers: IPNNetMapPeerNode[]
onSubmit: (def: SSHSessionDef) => void
onSubmit: (def: SSHFormSessionDef) => void
}) {
sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name))
const [username, setUsername] = useState("")
@ -99,7 +103,51 @@ function SSHForm({
type="submit"
class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
value="SSH"
onClick={(e) => {
if (e.altKey) {
e.preventDefault()
e.stopPropagation()
onSubmit({ username, hostname, newWindow: true })
}
}}
/>
</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.
resizeObserver = new ResizeObserver(() => fitAddon.fit())
resizeObserver =
new termContainerNode.ownerDocument.defaultView!.ResizeObserver(() =>
fitAddon.fit()
)
resizeObserver.observe(termContainerNode)
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"
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
xterm@^4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1"
integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==
xterm@mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67:
version "4.19.0"
resolved "https://codeload.github.com/mihaip/xterm.js/tar.gz/95100e3c870b59348bb4fa5504bab9f9317bac67"
y18n@^4.0.0:
version "4.0.3"