feat: add member terminal trigger and align terminal panel bottom-center

This commit is contained in:
2025-09-30 21:32:30 +02:00
parent 75dc122898
commit a26ef3949a
3 changed files with 104 additions and 38 deletions

View File

@@ -23,6 +23,9 @@ class ClusterMembersComponent extends Component {
// Drawer state for desktop // Drawer state for desktop
this.drawer = new DrawerComponent(); this.drawer = new DrawerComponent();
// Terminal panel container (shared with drawer)
this.terminalPanelContainer = null;
// Selection state for highlighting // Selection state for highlighting
this.selectedMemberIp = null; this.selectedMemberIp = null;
} }
@@ -403,7 +406,6 @@ class ClusterMembersComponent extends Component {
const membersHTML = members.map(member => { const membersHTML = members.map(member => {
const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 'status-offline'; 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') ? '🟢' : '🔴'; const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : '🔴';
logger.debug('ClusterMembersComponent: Rendering member:', member); logger.debug('ClusterMembersComponent: Rendering member:', member);
@@ -433,10 +435,18 @@ class ClusterMembersComponent extends Component {
</div> </div>
` : ''} ` : ''}
</div> </div>
<div class="expand-icon"> <div class="member-actions">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <button class="member-terminal-btn" title="Open Terminal" aria-label="Open Terminal">
<path d="M6 9l6 6 6-6"/> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
</svg> <path d="M4 17l6-6-6-6"></path>
<path d="M12 19h8"></path>
</svg>
</button>
<div class="expand-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</div>
</div> </div>
</div> </div>
<div class="member-details"> <div class="member-details">
@@ -455,6 +465,7 @@ class ClusterMembersComponent extends Component {
setupMemberCards(members) { setupMemberCards(members) {
setTimeout(() => { setTimeout(() => {
this.findAllElements('.member-card').forEach((card, index) => { this.findAllElements('.member-card').forEach((card, index) => {
const terminalBtn = card.querySelector('.member-terminal-btn');
const expandIcon = card.querySelector('.expand-icon'); const expandIcon = card.querySelector('.expand-icon');
const memberDetails = card.querySelector('.member-details'); const memberDetails = card.querySelector('.member-details');
const memberIp = card.dataset.memberIp; 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); }, 100);
} }
@@ -722,6 +748,20 @@ class ClusterMembersComponent extends Component {
this.selectedMemberIp = null; 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; window.ClusterMembersComponent = ClusterMembersComponent;

View File

@@ -21,11 +21,13 @@
this._buildPanel(); this._buildPanel();
} }
// Adjust position to be left of the details drawer // Reset any leftover inline positioning so CSS centering applies
this._adjustRightOffset(); if (this.container) {
if (!this.resizeHandler) { this.container.style.right = '';
this.resizeHandler = () => this._adjustRightOffset(); this.container.style.left = '';
window.addEventListener('resize', this.resizeHandler); this.container.style.top = '';
this.container.style.bottom = '';
this.container.style.width = '';
} }
// Show panel with animation // Show panel with animation
@@ -193,17 +195,6 @@
return text; 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 // Expose singleton API

View File

@@ -333,12 +333,46 @@ p {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: 0.75rem;
} }
.member-info { .member-info {
flex: 1; 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 { .expand-icon {
color: var(--text-tertiary); color: var(--text-tertiary);
cursor: pointer; cursor: pointer;
@@ -3340,40 +3374,41 @@ select.param-input:focus {
.details-drawer-backdrop { display: none; } .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 { .terminal-panel-container {
position: fixed; position: fixed;
inset: auto 0 0 auto; /* bottom-right anchored; right adjusted dynamically */ inset: 0;
right: 0; /* updated at runtime to drawer width */ display: flex;
width: clamp(33.333vw, 600px, 90vw); /* match drawer width by default */ align-items: flex-end;
z-index: 1001; /* above drawer (1000) and backdrop (999) */ justify-content: center;
pointer-events: none; /* allow clicks only on panel */ padding: 1.5rem;
z-index: 1001;
pointer-events: none;
} }
.terminal-panel { .terminal-panel {
position: absolute; position: relative;
left: 0; width: min(720px, 90vw);
right: 0; height: min(45vh, 520px);
bottom: 0; max-height: 65vh;
height: 33vh; /* 1/3 of viewport height */
max-height: 40vh;
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
border-top: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-left: 1px solid var(--border-primary); box-shadow: 0 18px 40px rgba(0,0,0,0.35);
box-shadow: 0 -8px 20px rgba(0,0,0,0.25); transform: translateY(32px);
transform: translateY(100%);
opacity: 0; opacity: 0;
transition: transform 0.25s ease, opacity 0.25s ease; transition: transform 0.25s ease, opacity 0.25s ease;
border-top-left-radius: 10px; border-radius: 12px;
pointer-events: auto; pointer-events: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.terminal-panel.visible { .terminal-panel.visible {
transform: translateY(0); transform: translateY(0);
opacity: 1; opacity: 1;
} }
.terminal-header { .terminal-header {
display: flex; display: flex;
align-items: center; align-items: center;