178 lines
6.5 KiB
JavaScript
178 lines
6.5 KiB
JavaScript
// Reusable Drawer Component for desktop slide-in panels
|
|
class DrawerComponent {
|
|
constructor() {
|
|
if (window.__sharedDrawerInstance) {
|
|
return window.__sharedDrawerInstance;
|
|
}
|
|
|
|
this.detailsDrawer = null;
|
|
this.detailsDrawerContent = null;
|
|
this.activeDrawerComponent = null;
|
|
this.onCloseCallback = null;
|
|
|
|
window.__sharedDrawerInstance = this;
|
|
}
|
|
|
|
// Determine if we should use desktop drawer behavior
|
|
isDesktop() {
|
|
try {
|
|
return window && window.innerWidth >= 1024; // desktop threshold
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ensureDrawer() {
|
|
if (this.detailsDrawer) return;
|
|
|
|
// Create drawer
|
|
this.detailsDrawer = document.createElement('div');
|
|
this.detailsDrawer.className = 'details-drawer';
|
|
|
|
// 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>
|
|
<div class="drawer-actions">
|
|
<button class="drawer-terminal-btn" title="Open Terminal" aria-label="Open Terminal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M4 17l6-6-6-6"></path>
|
|
<path d="M12 19h8"></path>
|
|
</svg>
|
|
</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>
|
|
</div>
|
|
`;
|
|
this.detailsDrawer.appendChild(header);
|
|
|
|
// Content container
|
|
this.detailsDrawerContent = document.createElement('div');
|
|
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');
|
|
if (!window.TerminalPanel) return;
|
|
const panel = window.TerminalPanel;
|
|
const wasMinimized = panel.isMinimized;
|
|
panel.open(this.terminalPanelContainer, nodeIp);
|
|
if (nodeIp && panel._updateTitle) {
|
|
panel._updateTitle(nodeIp);
|
|
}
|
|
if (wasMinimized && panel.restore) {
|
|
panel.restore();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to open terminal:', err);
|
|
}
|
|
});
|
|
}
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') close();
|
|
});
|
|
}
|
|
|
|
openDrawer(title, contentCallback, errorCallback, onCloseCallback, hideTerminalButton = false) {
|
|
this.ensureDrawer();
|
|
this.onCloseCallback = onCloseCallback;
|
|
|
|
// Set drawer title
|
|
const titleEl = this.detailsDrawer.querySelector('.drawer-title');
|
|
if (titleEl) {
|
|
titleEl.textContent = title;
|
|
}
|
|
|
|
// Show/hide terminal button based on parameter
|
|
const terminalBtn = this.detailsDrawer.querySelector('.drawer-terminal-btn');
|
|
if (terminalBtn) {
|
|
terminalBtn.style.display = hideTerminalButton ? 'none' : 'block';
|
|
}
|
|
|
|
// Clear previous component if any
|
|
if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') {
|
|
try {
|
|
this.activeDrawerComponent.unmount();
|
|
} catch (_) {}
|
|
}
|
|
this.detailsDrawerContent.innerHTML = '<div class="loading-details">Loading detailed information...</div>';
|
|
|
|
// Execute content callback
|
|
try {
|
|
contentCallback(this.detailsDrawerContent, (component) => {
|
|
this.activeDrawerComponent = component;
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to load drawer content:', error);
|
|
if (errorCallback) {
|
|
errorCallback(error);
|
|
} else {
|
|
this.detailsDrawerContent.innerHTML = `
|
|
<div class="error">
|
|
<strong>Error loading content:</strong><br>
|
|
${this.escapeHtml ? this.escapeHtml(error.message) : error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Open drawer
|
|
this.detailsDrawer.classList.add('open');
|
|
// Inform terminal container that the drawer is open for alignment
|
|
if (this.terminalPanelContainer) {
|
|
this.terminalPanelContainer.classList.add('drawer-open');
|
|
}
|
|
}
|
|
|
|
closeDrawer() {
|
|
if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
|
|
if (this.terminalPanelContainer) {
|
|
this.terminalPanelContainer.classList.remove('drawer-open');
|
|
}
|
|
|
|
// Call close callback if provided
|
|
if (this.onCloseCallback) {
|
|
this.onCloseCallback();
|
|
this.onCloseCallback = null;
|
|
}
|
|
}
|
|
|
|
// Clean up drawer elements
|
|
destroy() {
|
|
if (this.detailsDrawer && this.detailsDrawer.parentNode) {
|
|
this.detailsDrawer.parentNode.removeChild(this.detailsDrawer);
|
|
}
|
|
this.detailsDrawer = null;
|
|
this.detailsDrawerContent = null;
|
|
this.activeDrawerComponent = null;
|
|
}
|
|
|
|
// Helper method for HTML escaping (can be overridden)
|
|
escapeHtml(text) {
|
|
if (typeof text !== 'string') return text;
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
window.DrawerComponent = DrawerComponent;
|