feat(terminal): add Terminal panel to Node Details with WebSocket

- Add 'Terminal' button to drawer header

- Implement TerminalPanel (bottom-up fade-in, ~1/3 viewport, left of drawer)

- Connect to ws(s)://{nodeIp}/ws; display incoming messages; input sends raw

- Wire Drawer to pass node IP and close terminal on drawer close

- Add styles/z-index and include script in index.html
This commit is contained in:
2025-09-29 21:44:16 +02:00
parent 602a3d6215
commit 9e3ab73a73
4 changed files with 335 additions and 3 deletions

View File

@@ -29,16 +29,19 @@ class DrawerComponent {
this.detailsDrawer = document.createElement('div');
this.detailsDrawer.className = 'details-drawer';
// Header with close button
// Header with actions and close button
const header = document.createElement('div');
header.className = 'details-drawer-header';
header.innerHTML = `
<div class="drawer-title">Node Details</div>
<button class="drawer-close" aria-label="Close">
<div class="drawer-actions">
<button class="drawer-terminal-btn" title="Open Terminal" aria-label="Open Terminal">Terminal</button>
<button class="drawer-close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</button>
</div>
`;
this.detailsDrawer.appendChild(header);
@@ -47,11 +50,28 @@ class DrawerComponent {
this.detailsDrawerContent.className = 'details-drawer-content';
this.detailsDrawer.appendChild(this.detailsDrawerContent);
// Terminal panel container (positioned left of details drawer)
this.terminalPanelContainer = document.createElement('div');
this.terminalPanelContainer.className = 'terminal-panel-container';
document.body.appendChild(this.terminalPanelContainer);
document.body.appendChild(this.detailsDrawer);
// Close handlers
const close = () => this.closeDrawer();
header.querySelector('.drawer-close').addEventListener('click', close);
const terminalBtn = header.querySelector('.drawer-terminal-btn');
if (terminalBtn) {
terminalBtn.addEventListener('click', (e) => {
e.stopPropagation();
try {
const nodeIp = this.activeDrawerComponent && this.activeDrawerComponent.viewModel && this.activeDrawerComponent.viewModel.get('nodeIp');
window.TerminalPanel && window.TerminalPanel.open(this.terminalPanelContainer, nodeIp);
} catch (err) {
console.error('Failed to open terminal:', err);
}
});
}
this.detailsDrawerBackdrop.addEventListener('click', close);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') close();
@@ -104,6 +124,9 @@ class DrawerComponent {
if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible');
// Also close terminal panel if open
try { if (window.TerminalPanel) window.TerminalPanel.close(); } catch (_) {}
// Call close callback if provided
if (this.onCloseCallback) {
this.onCloseCallback();