Files
threshold-ui/ui/desktop/app/components/session/tabs/index.js
T
LaurenandZhihe Li 990e2d6b9d [Desktop][Bug fix]: Stop target sessions before unauthenticating user session (#2891)
* add stopAll invocation to ipc handler

* trigger modal on both signout btns

* refactor invalidateSession fn

* fix application controller tests

* fix failing tests settings-card/user

* add test to Acceptance | authentication

* migrate electron dialog.showMessageBoxSync to Hds

* rename channel and tracked prop, consolidate cancel fns

* combine modals, add willDestroy, refactor

* test signout and close sessions modal

* address feedback

* refactor window.electron in authentication-test

* address feedback

* refactor: 💡 Fix failing tests

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2025-08-04 16:44:27 -04:00

141 lines
4.6 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { CanvasAddon } from '@xterm/addon-canvas';
import { v4 as uuidv4 } from 'uuid';
import debounce from 'lodash/debounce';
const terminalOptions = {
cursorBlink: true,
};
// This dynamically sets the terminal container's height inline so we have an accurate
// height for the terminal container. This is necessary because we don't have an
// intrinsic height from our parent containers and we don't have any content yet from
// the terminal to have a usable height for the container. This means that calling `.fit()`
// doesn't get an accurate representation of what the container size can actually be
const calculateTerminalContainerHeight = (termContainer) => {
const viewportHeight = window.innerHeight;
// Calculate the height by getting the viewport height, subtracting what
// the top offset from the container is, and leaving a small padding to
// also prevent any overflow scrolling on the container
const height = viewportHeight - termContainer.offsetTop - 24;
termContainer.style.height = `${height}px`;
};
export default class SessionTerminalTabsComponent extends Component {
// =services
@service ipc;
// =attributes
id;
terminal;
removeTerminalListener;
// =actions
@action
async openTerminal() {
// Don't re-initialize the terminal if it's already been opened
if (this.terminal) {
return;
}
const { isWindows } = await this.ipc.invoke('checkOS');
const xterm = new Terminal(terminalOptions);
this.terminal = xterm;
const fitAddon = new FitAddon();
const termContainer = document.getElementById(`terminal-container`);
calculateTerminalContainerHeight(termContainer);
/**
* Custom key event handler for windows to achieve Ctrl+C as on the other OS's
*/
if (isWindows) {
xterm.attachCustomKeyEventHandler(async (arg) => {
if (arg.ctrlKey && arg.code === 'KeyC' && arg.type === 'keydown') {
const selection = xterm.getSelection();
if (selection) {
await navigator.clipboard.writeText(selection);
return false;
}
}
return true;
});
}
xterm.loadAddon(fitAddon);
xterm.loadAddon(new CanvasAddon());
xterm.open(termContainer);
fitAddon.fit();
xterm.focus();
// Generate a UUID to have the terminal handlers be unique
this.id = uuidv4();
this.#setupTerminal(fitAddon, xterm, termContainer);
const isSSHCommandAvailable = await this.ipc.invoke('checkCommand', 'ssh');
const { model } = this.args;
const { proxy_address, proxy_port, started_desktop_client, target } = model;
// Only send the command in certain scenarios:
// 1. Only for SSH targets
// 2. Don't connect if the session wasn't initiated in the desktop client
// which means we won't have proxy information
// 3. Only connect if the user has an SSH client available in their path
if (target?.isSSH && started_desktop_client && isSSHCommandAvailable) {
// Send an SSH command immediately
window.terminal.send(
`ssh ${proxy_address} -p ${proxy_port} -o NoHostAuthenticationForLocalhost=yes\r`,
this.id,
);
}
}
willDestroy() {
super.willDestroy(...arguments);
// Kill terminal and clean up event listeners
if (this.terminal) {
this.terminal.dispose();
}
window.terminal?.remove(this.id);
window.onresize = null;
this.removeTerminalListener?.();
}
#setupTerminal(fitAddon, xterm, termContainer) {
// Terminal is exposed by contextBridge within the preload script
window.terminal.create({ id: this.id, cols: xterm.cols, rows: xterm.rows });
xterm.onData((data) => window.terminal.send(data, this.id));
// Save the handler to cleanup the listener on the renderer process later
this.removeTerminalListener = window.terminal.receive((value) => {
xterm.write(value);
}, this.id);
// Handle resizing terminal windows. We debounce the resizing as we don't want
// to resize xterm and the pty process before the previous one has finished
const debouncedFit = debounce(() => {
calculateTerminalContainerHeight(termContainer);
fitAddon.fit();
}, 150);
window.onresize = () => {
debouncedFit();
};
xterm.onResize((size) => {
window.terminal.resize(size, this.id);
});
}
}