diff --git a/public/scripts/components/ClusterMembersComponent.js b/public/scripts/components/ClusterMembersComponent.js index ff5f222..f68e49b 100644 --- a/public/scripts/components/ClusterMembersComponent.js +++ b/public/scripts/components/ClusterMembersComponent.js @@ -23,6 +23,9 @@ class ClusterMembersComponent extends Component { // Drawer state for desktop this.drawer = new DrawerComponent(); + // Terminal panel container (shared with drawer) + this.terminalPanelContainer = null; + // Selection state for highlighting this.selectedMemberIp = null; } @@ -403,7 +406,6 @@ class ClusterMembersComponent extends Component { const membersHTML = members.map(member => { const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 'status-offline'; - const statusText = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'Online' : 'Offline'; const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : '🔴'; logger.debug('ClusterMembersComponent: Rendering member:', member); @@ -433,10 +435,18 @@ class ClusterMembersComponent extends Component { ` : ''} -
- - - +
+ +
+ + + +
@@ -455,6 +465,7 @@ class ClusterMembersComponent extends Component { setupMemberCards(members) { setTimeout(() => { this.findAllElements('.member-card').forEach((card, index) => { + const terminalBtn = card.querySelector('.member-terminal-btn'); const expandIcon = card.querySelector('.expand-icon'); const memberDetails = card.querySelector('.member-details'); const memberIp = card.dataset.memberIp; @@ -505,6 +516,21 @@ class ClusterMembersComponent extends Component { } }); } + + if (terminalBtn) { + this.addEventListener(terminalBtn, 'click', (e) => { + e.stopPropagation(); + e.preventDefault(); + try { + if (window.TerminalPanel) { + this.ensureTerminalContainer(); + window.TerminalPanel.open(this.terminalPanelContainer, memberIp); + } + } catch (err) { + console.error('Failed to open member terminal:', err); + } + }); + } }); }, 100); } @@ -722,6 +748,20 @@ class ClusterMembersComponent extends Component { this.selectedMemberIp = null; } } + + ensureTerminalContainer() { + if (!this.terminalPanelContainer) { + try { + const drawer = this.drawer; + if (drawer && drawer.ensureDrawer) { + drawer.ensureDrawer(); + this.terminalPanelContainer = drawer.terminalPanelContainer; + } + } catch (err) { + console.error('Failed to ensure terminal container:', err); + } + } + } } window.ClusterMembersComponent = ClusterMembersComponent; \ No newline at end of file diff --git a/public/scripts/components/TerminalPanelComponent.js b/public/scripts/components/TerminalPanelComponent.js index 50403d1..922baff 100644 --- a/public/scripts/components/TerminalPanelComponent.js +++ b/public/scripts/components/TerminalPanelComponent.js @@ -21,11 +21,13 @@ this._buildPanel(); } - // Adjust position to be left of the details drawer - this._adjustRightOffset(); - if (!this.resizeHandler) { - this.resizeHandler = () => this._adjustRightOffset(); - window.addEventListener('resize', this.resizeHandler); + // Reset any leftover inline positioning so CSS centering applies + if (this.container) { + this.container.style.right = ''; + this.container.style.left = ''; + this.container.style.top = ''; + this.container.style.bottom = ''; + this.container.style.width = ''; } // Show panel with animation @@ -193,17 +195,6 @@ return text; } } - - _adjustRightOffset() { - try { - const drawer = document.querySelector('.details-drawer'); - const isOpen = drawer && drawer.classList.contains('open'); - const width = isOpen ? drawer.getBoundingClientRect().width : 0; - if (this.container) { - this.container.style.right = `${width}px`; - } - } catch (_) { /* no-op */ } - } } // Expose singleton API diff --git a/public/styles/main.css b/public/styles/main.css index 1a8123b..f05d446 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -333,12 +333,46 @@ p { display: flex; justify-content: space-between; align-items: flex-start; + gap: 0.75rem; } .member-info { flex: 1; } +.member-actions { + display: flex; + align-items: center; + gap: 0.4rem; +} + +.member-terminal-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + border-radius: 6px; + border: 1px solid var(--border-primary); + background: rgba(255, 255, 255, 0.05); + color: var(--text-tertiary); + cursor: pointer; + transition: all 0.2s ease; +} + +.member-terminal-btn:hover, +.member-terminal-btn:focus-visible { + background: rgba(255, 255, 255, 0.12); + color: var(--text-secondary); + border-color: rgba(255, 255, 255, 0.2); +} + +.member-terminal-btn svg { + width: 18px; + height: 18px; + stroke: currentColor; + stroke-width: 2; +} + .expand-icon { color: var(--text-tertiary); cursor: pointer; @@ -3340,40 +3374,41 @@ select.param-input:focus { .details-drawer-backdrop { display: none; } } -/* Terminal Panel - bottom up, left of drawer, 1/3 viewport height */ +/* Terminal Panel - bottom-centered modal style */ .terminal-panel-container { position: fixed; - inset: auto 0 0 auto; /* bottom-right anchored; right adjusted dynamically */ - right: 0; /* updated at runtime to drawer width */ - width: clamp(33.333vw, 600px, 90vw); /* match drawer width by default */ - z-index: 1001; /* above drawer (1000) and backdrop (999) */ - pointer-events: none; /* allow clicks only on panel */ + inset: 0; + display: flex; + align-items: flex-end; + justify-content: center; + padding: 1.5rem; + z-index: 1001; + pointer-events: none; } .terminal-panel { - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 33vh; /* 1/3 of viewport height */ - max-height: 40vh; + position: relative; + width: min(720px, 90vw); + height: min(45vh, 520px); + max-height: 65vh; background: var(--bg-primary); color: var(--text-primary); - border-top: 1px solid var(--border-primary); - border-left: 1px solid var(--border-primary); - box-shadow: 0 -8px 20px rgba(0,0,0,0.25); - transform: translateY(100%); + border: 1px solid var(--border-primary); + box-shadow: 0 18px 40px rgba(0,0,0,0.35); + transform: translateY(32px); opacity: 0; transition: transform 0.25s ease, opacity 0.25s ease; - border-top-left-radius: 10px; + border-radius: 12px; pointer-events: auto; display: flex; flex-direction: column; } + .terminal-panel.visible { transform: translateY(0); opacity: 1; } + .terminal-header { display: flex; align-items: center;