diff --git a/public/index.html b/public/index.html index d77de05..32651e1 100644 --- a/public/index.html +++ b/public/index.html @@ -146,6 +146,7 @@ + diff --git a/public/scripts/components/ClusterMembersComponent.js b/public/scripts/components/ClusterMembersComponent.js index fda4a19..7b89fb4 100644 --- a/public/scripts/components/ClusterMembersComponent.js +++ b/public/scripts/components/ClusterMembersComponent.js @@ -21,71 +21,23 @@ class ClusterMembersComponent extends Component { }, 200); // Drawer state for desktop - this.detailsDrawer = null; - this.detailsDrawerContent = null; - this.detailsDrawerBackdrop = null; - this.activeDrawerComponent = null; + this.drawer = new DrawerComponent(); } // Determine if we should use desktop drawer behavior isDesktop() { - try { - return window && window.innerWidth >= 1024; // desktop threshold - } catch (_) { - return false; - } + return this.drawer.isDesktop(); } - ensureDrawer() { - if (this.detailsDrawer) return; - // Create backdrop - this.detailsDrawerBackdrop = document.createElement('div'); - this.detailsDrawerBackdrop.className = 'details-drawer-backdrop'; - document.body.appendChild(this.detailsDrawerBackdrop); - - // Create drawer - this.detailsDrawer = document.createElement('div'); - this.detailsDrawer.className = 'details-drawer'; - - // Header with close button - const header = document.createElement('div'); - header.className = 'details-drawer-header'; - header.innerHTML = ` -
Node Details
- - `; - this.detailsDrawer.appendChild(header); - - // Content container - this.detailsDrawerContent = document.createElement('div'); - this.detailsDrawerContent.className = 'details-drawer-content'; - this.detailsDrawer.appendChild(this.detailsDrawerContent); - - document.body.appendChild(this.detailsDrawer); - - // Close handlers - const close = () => this.closeDrawer(); - header.querySelector('.drawer-close').addEventListener('click', close); - this.detailsDrawerBackdrop.addEventListener('click', close); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') close(); - }); - } openDrawerForMember(memberIp) { - this.ensureDrawer(); - - // Set drawer title to member name (hostname) and IP + // Get display name for drawer title + let displayName = memberIp; try { const members = (this.viewModel && typeof this.viewModel.get === 'function') ? this.viewModel.get('members') : []; const member = Array.isArray(members) ? members.find(m => m && m.ip === memberIp) : null; const hostname = (member && member.hostname) ? member.hostname : ''; const ip = (member && member.ip) ? member.ip : memberIp; - let displayName = memberIp; if (hostname && ip) { displayName = `${hostname} - ${ip}`; @@ -94,46 +46,33 @@ class ClusterMembersComponent extends Component { } else if (ip) { displayName = ip; } - - const titleEl = this.detailsDrawer.querySelector('.drawer-title'); - if (titleEl) { - titleEl.textContent = displayName; - } } catch (_) { // no-op if anything goes wrong, default title remains } - // Clear previous component if any - if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') { - try { this.activeDrawerComponent.unmount(); } catch (_) {} - } - this.detailsDrawerContent.innerHTML = '
Loading detailed information...
'; + // Open drawer with content callback + this.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => { + // Load and mount NodeDetails into drawer + const nodeDetailsVM = new NodeDetailsViewModel(); + const nodeDetailsComponent = new NodeDetailsComponent(contentContainer, nodeDetailsVM, this.eventBus); + setActiveComponent(nodeDetailsComponent); - // Load and mount NodeDetails into drawer - const nodeDetailsVM = new NodeDetailsViewModel(); - const nodeDetailsComponent = new NodeDetailsComponent(this.detailsDrawerContent, nodeDetailsVM, this.eventBus); - this.activeDrawerComponent = nodeDetailsComponent; - - nodeDetailsVM.loadNodeDetails(memberIp).then(() => { - nodeDetailsComponent.mount(); - }).catch((error) => { - logger.error('Failed to load node details for drawer:', error); - this.detailsDrawerContent.innerHTML = ` -
- Error loading node details:
- ${this.escapeHtml(error.message)} -
- `; + nodeDetailsVM.loadNodeDetails(memberIp).then(() => { + nodeDetailsComponent.mount(); + }).catch((error) => { + logger.error('Failed to load node details for drawer:', error); + contentContainer.innerHTML = ` +
+ Error loading node details:
+ ${this.escapeHtml(error.message)} +
+ `; + }); }); - - // Open drawer - this.detailsDrawer.classList.add('open'); - this.detailsDrawerBackdrop.classList.add('visible'); } closeDrawer() { - if (this.detailsDrawer) this.detailsDrawer.classList.remove('open'); - if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible'); + this.drawer.closeDrawer(); } mount() { diff --git a/public/scripts/components/ComponentsLoader.js b/public/scripts/components/ComponentsLoader.js index 4e8b580..a9d7230 100644 --- a/public/scripts/components/ComponentsLoader.js +++ b/public/scripts/components/ComponentsLoader.js @@ -1,7 +1,7 @@ (function(){ // Simple readiness flag once all component constructors are present function allReady(){ - return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent); + return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent); } window.waitForComponentsReady = function(timeoutMs = 5000){ return new Promise((resolve, reject) => { diff --git a/public/scripts/components/DrawerComponent.js b/public/scripts/components/DrawerComponent.js new file mode 100644 index 0000000..3f8930f --- /dev/null +++ b/public/scripts/components/DrawerComponent.js @@ -0,0 +1,130 @@ +// Reusable Drawer Component for desktop slide-in panels +class DrawerComponent { + constructor() { + this.detailsDrawer = null; + this.detailsDrawerContent = null; + this.detailsDrawerBackdrop = null; + this.activeDrawerComponent = null; + } + + // 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 backdrop + this.detailsDrawerBackdrop = document.createElement('div'); + this.detailsDrawerBackdrop.className = 'details-drawer-backdrop'; + document.body.appendChild(this.detailsDrawerBackdrop); + + // Create drawer + this.detailsDrawer = document.createElement('div'); + this.detailsDrawer.className = 'details-drawer'; + + // Header with close button + const header = document.createElement('div'); + header.className = 'details-drawer-header'; + header.innerHTML = ` +
Node Details
+ + `; + this.detailsDrawer.appendChild(header); + + // Content container + this.detailsDrawerContent = document.createElement('div'); + this.detailsDrawerContent.className = 'details-drawer-content'; + this.detailsDrawer.appendChild(this.detailsDrawerContent); + + document.body.appendChild(this.detailsDrawer); + + // Close handlers + const close = () => this.closeDrawer(); + header.querySelector('.drawer-close').addEventListener('click', close); + this.detailsDrawerBackdrop.addEventListener('click', close); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') close(); + }); + } + + openDrawer(title, contentCallback, errorCallback) { + this.ensureDrawer(); + + // Set drawer title + const titleEl = this.detailsDrawer.querySelector('.drawer-title'); + if (titleEl) { + titleEl.textContent = title; + } + + // Clear previous component if any + if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') { + try { + this.activeDrawerComponent.unmount(); + } catch (_) {} + } + this.detailsDrawerContent.innerHTML = '
Loading detailed information...
'; + + // 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 = ` +
+ Error loading content:
+ ${this.escapeHtml ? this.escapeHtml(error.message) : error.message} +
+ `; + } + } + + // Open drawer + this.detailsDrawer.classList.add('open'); + this.detailsDrawerBackdrop.classList.add('visible'); + } + + closeDrawer() { + if (this.detailsDrawer) this.detailsDrawer.classList.remove('open'); + if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible'); + } + + // Clean up drawer elements + destroy() { + if (this.detailsDrawer && this.detailsDrawer.parentNode) { + this.detailsDrawer.parentNode.removeChild(this.detailsDrawer); + } + if (this.detailsDrawerBackdrop && this.detailsDrawerBackdrop.parentNode) { + this.detailsDrawerBackdrop.parentNode.removeChild(this.detailsDrawerBackdrop); + } + + this.detailsDrawer = null; + this.detailsDrawerContent = null; + this.detailsDrawerBackdrop = 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; diff --git a/public/scripts/components/TopologyGraphComponent.js b/public/scripts/components/TopologyGraphComponent.js index 2c9a3b3..0f33c29 100644 --- a/public/scripts/components/TopologyGraphComponent.js +++ b/public/scripts/components/TopologyGraphComponent.js @@ -11,10 +11,7 @@ class TopologyGraphComponent extends Component { this.isInitialized = false; // Drawer state for desktop reuse (same pattern as ClusterMembersComponent) - this.detailsDrawer = null; - this.detailsDrawerContent = null; - this.detailsDrawerBackdrop = null; - this.activeDrawerComponent = null; + this.drawer = new DrawerComponent(); // Tooltip for labels on hover this.tooltipEl = null; @@ -22,52 +19,16 @@ class TopologyGraphComponent extends Component { // Determine desktop threshold isDesktop() { - try { return window && window.innerWidth >= 1024; } catch (_) { return false; } + return this.drawer.isDesktop(); } - ensureDrawer() { - if (this.detailsDrawer) return; - // Backdrop - this.detailsDrawerBackdrop = document.createElement('div'); - this.detailsDrawerBackdrop.className = 'details-drawer-backdrop'; - document.body.appendChild(this.detailsDrawerBackdrop); - - // Drawer - this.detailsDrawer = document.createElement('div'); - this.detailsDrawer.className = 'details-drawer'; - - const header = document.createElement('div'); - header.className = 'details-drawer-header'; - header.innerHTML = ` -
Node Details
- - `; - this.detailsDrawer.appendChild(header); - - this.detailsDrawerContent = document.createElement('div'); - this.detailsDrawerContent.className = 'details-drawer-content'; - this.detailsDrawer.appendChild(this.detailsDrawerContent); - - document.body.appendChild(this.detailsDrawer); - - const close = () => this.closeDrawer(); - header.querySelector('.drawer-close').addEventListener('click', close); - this.detailsDrawerBackdrop.addEventListener('click', close); - document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); - } openDrawerForNode(nodeData) { - this.ensureDrawer(); - - // Title from hostname and IP + // Get display name for drawer title + let displayName = 'Node Details'; try { const hostname = nodeData.hostname || ''; const ip = nodeData.ip || ''; - let displayName = 'Node Details'; if (hostname && ip) { displayName = `${hostname} - ${ip}`; @@ -76,43 +37,32 @@ class TopologyGraphComponent extends Component { } else if (ip) { displayName = ip; } - - const titleEl = this.detailsDrawer.querySelector('.drawer-title'); - if (titleEl) titleEl.textContent = displayName; } catch (_) {} - // Clear previous component - if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') { - try { this.activeDrawerComponent.unmount(); } catch (_) {} - } - this.detailsDrawerContent.innerHTML = '
Loading detailed information...
'; + // Open drawer with content callback + this.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => { + // Mount NodeDetailsComponent + const nodeDetailsVM = new NodeDetailsViewModel(); + const nodeDetailsComponent = new NodeDetailsComponent(contentContainer, nodeDetailsVM, this.eventBus); + setActiveComponent(nodeDetailsComponent); - // Mount NodeDetailsComponent - const nodeDetailsVM = new NodeDetailsViewModel(); - const nodeDetailsComponent = new NodeDetailsComponent(this.detailsDrawerContent, nodeDetailsVM, this.eventBus); - this.activeDrawerComponent = nodeDetailsComponent; - - const ip = nodeData.ip || nodeData.id; - nodeDetailsVM.loadNodeDetails(ip).then(() => { - nodeDetailsComponent.mount(); - }).catch((error) => { - logger.error('Failed to load node details (topology drawer):', error); - this.detailsDrawerContent.innerHTML = ` -
- Error loading node details:
- ${this.escapeHtml(error.message)} -
- `; + const ip = nodeData.ip || nodeData.id; + nodeDetailsVM.loadNodeDetails(ip).then(() => { + nodeDetailsComponent.mount(); + }).catch((error) => { + logger.error('Failed to load node details (topology drawer):', error); + contentContainer.innerHTML = ` +
+ Error loading node details:
+ ${this.escapeHtml(error.message)} +
+ `; + }); }); - - // Open - this.detailsDrawer.classList.add('open'); - this.detailsDrawerBackdrop.classList.add('visible'); } closeDrawer() { - if (this.detailsDrawer) this.detailsDrawer.classList.remove('open'); - if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible'); + this.drawer.closeDrawer(); } // Tooltip helpers