feat: add labels
This commit is contained in:
@@ -87,7 +87,16 @@ class NodeDetailsComponent extends Component {
|
|||||||
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
||||||
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
||||||
|
|
||||||
|
// Build labels bar (above tabs)
|
||||||
|
const labelsObj = (nodeStatus && nodeStatus.labels) ? nodeStatus.labels : null;
|
||||||
|
const labelsBar = (labelsObj && Object.keys(labelsObj).length)
|
||||||
|
? `<div class="member-labels" style="margin: 0 0 12px 0;">${Object.entries(labelsObj)
|
||||||
|
.map(([k, v]) => `<span class=\"label-chip\">${this.escapeHtml(String(k))}: ${this.escapeHtml(String(v))}</span>`)
|
||||||
|
.join('')}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
|
${labelsBar}
|
||||||
<div class="tabs-container">
|
<div class="tabs-container">
|
||||||
<div class="tabs-header">
|
<div class="tabs-header">
|
||||||
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
|
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
|
||||||
|
|||||||
@@ -9,6 +9,96 @@ class TopologyGraphComponent extends Component {
|
|||||||
this.width = 0; // Will be set dynamically based on container size
|
this.width = 0; // Will be set dynamically based on container size
|
||||||
this.height = 0; // Will be set dynamically based on container size
|
this.height = 0; // Will be set dynamically based on container size
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
|
// Drawer state for desktop reuse (same pattern as ClusterMembersComponent)
|
||||||
|
this.detailsDrawer = null;
|
||||||
|
this.detailsDrawerContent = null;
|
||||||
|
this.detailsDrawerBackdrop = null;
|
||||||
|
this.activeDrawerComponent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine desktop threshold
|
||||||
|
isDesktop() {
|
||||||
|
try { return window && window.innerWidth >= 1024; } catch (_) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="drawer-title">Node Details</div>
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
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 or IP
|
||||||
|
try {
|
||||||
|
const displayName = nodeData.hostname || nodeData.ip || 'Node Details';
|
||||||
|
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 = '<div class="loading-details">Loading detailed information...</div>';
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="error">
|
||||||
|
<strong>Error loading node details:</strong><br>
|
||||||
|
${this.escapeHtml(error.message)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDimensions(container) {
|
updateDimensions(container) {
|
||||||
@@ -345,7 +435,13 @@ class TopologyGraphComponent extends Component {
|
|||||||
node.on('click', (event, d) => {
|
node.on('click', (event, d) => {
|
||||||
this.viewModel.selectNode(d.id);
|
this.viewModel.selectNode(d.id);
|
||||||
this.updateSelection(d.id);
|
this.updateSelection(d.id);
|
||||||
|
if (this.isDesktop()) {
|
||||||
|
// Desktop: open slide-in drawer, reuse NodeDetailsComponent
|
||||||
|
this.openDrawerForNode(d);
|
||||||
|
} else {
|
||||||
|
// Mobile/low-res: keep existing overlay
|
||||||
this.showMemberCardOverlay(d);
|
this.showMemberCardOverlay(d);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
node.on('mouseover', (event, d) => {
|
node.on('mouseover', (event, d) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user