537 lines
23 KiB
JavaScript
537 lines
23 KiB
JavaScript
// Monitoring View Component
|
|
class MonitoringViewComponent extends Component {
|
|
constructor(container, viewModel, eventBus) {
|
|
super(container, viewModel, eventBus);
|
|
|
|
logger.debug('MonitoringViewComponent: Constructor called');
|
|
logger.debug('MonitoringViewComponent: Container:', container);
|
|
logger.debug('MonitoringViewComponent: Container ID:', container?.id);
|
|
|
|
// Track if we've already loaded data to prevent unnecessary reloads
|
|
this.dataLoaded = false;
|
|
|
|
// Drawer state for desktop
|
|
this.drawer = new DrawerComponent();
|
|
}
|
|
|
|
mount() {
|
|
logger.debug('MonitoringViewComponent: Mounting...');
|
|
super.mount();
|
|
|
|
// Set up refresh button event listener
|
|
this.setupRefreshButton();
|
|
|
|
// Only load data if we haven't already or if the view model is empty
|
|
const clusterMembers = this.viewModel.get('clusterMembers');
|
|
if (!this.dataLoaded || !clusterMembers || clusterMembers.length === 0) {
|
|
this.loadData();
|
|
}
|
|
|
|
// Subscribe to view model changes
|
|
this.setupSubscriptions();
|
|
}
|
|
|
|
setupRefreshButton() {
|
|
const refreshBtn = this.findElement('#refresh-monitoring-btn');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', () => {
|
|
this.refreshData();
|
|
});
|
|
}
|
|
}
|
|
|
|
setupSubscriptions() {
|
|
// Subscribe to cluster members changes
|
|
this.viewModel.subscribe('clusterMembers', () => {
|
|
this.render();
|
|
});
|
|
|
|
// Subscribe to node resources changes
|
|
this.viewModel.subscribe('nodeResources', () => {
|
|
this.render();
|
|
});
|
|
|
|
// Subscribe to cluster summary changes
|
|
this.viewModel.subscribe('clusterSummary', () => {
|
|
this.render();
|
|
});
|
|
|
|
// Subscribe to loading state changes
|
|
this.viewModel.subscribe('isLoading', () => {
|
|
this.render();
|
|
});
|
|
|
|
// Subscribe to error changes
|
|
this.viewModel.subscribe('error', () => {
|
|
this.render();
|
|
});
|
|
}
|
|
|
|
async loadData() {
|
|
logger.debug('MonitoringViewComponent: Loading data...');
|
|
this.dataLoaded = true;
|
|
await this.viewModel.loadClusterData();
|
|
}
|
|
|
|
async refreshData() {
|
|
logger.debug('MonitoringViewComponent: Refreshing data...');
|
|
await this.viewModel.refresh();
|
|
}
|
|
|
|
// Determine if we should use desktop drawer behavior
|
|
isDesktop() {
|
|
return this.drawer.isDesktop();
|
|
}
|
|
|
|
// Open drawer for a specific node
|
|
openDrawerForNode(nodeData) {
|
|
const { ip, hostname } = nodeData;
|
|
|
|
// Get display name for drawer title
|
|
let displayName = ip;
|
|
if (hostname && ip) {
|
|
displayName = `${hostname} - ${ip}`;
|
|
} else if (hostname) {
|
|
displayName = hostname;
|
|
} else if (ip) {
|
|
displayName = ip;
|
|
}
|
|
|
|
// 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);
|
|
|
|
nodeDetailsVM.loadNodeDetails(ip).then(() => {
|
|
nodeDetailsComponent.mount();
|
|
}).catch((error) => {
|
|
logger.error('Failed to load node details for drawer:', error);
|
|
contentContainer.innerHTML = `
|
|
<div class="error">
|
|
<strong>Error loading node details:</strong><br>
|
|
${this.escapeHtml(error.message)}
|
|
</div>
|
|
`;
|
|
});
|
|
});
|
|
}
|
|
|
|
closeDrawer() {
|
|
this.drawer.closeDrawer();
|
|
}
|
|
|
|
// Get color class based on utilization percentage (same logic as gauges)
|
|
getUtilizationColorClass(percentage) {
|
|
const numPercentage = parseFloat(percentage);
|
|
|
|
if (numPercentage === 0 || isNaN(numPercentage)) return 'utilization-empty';
|
|
if (numPercentage < 50) return 'utilization-green';
|
|
if (numPercentage < 80) return 'utilization-yellow';
|
|
return 'utilization-red';
|
|
}
|
|
|
|
// Format lastSeen timestamp to human readable format
|
|
formatLastSeen(lastSeen) {
|
|
if (!lastSeen) return 'Unknown';
|
|
|
|
// lastSeen appears to be in milliseconds
|
|
const now = Date.now();
|
|
const diff = now - lastSeen;
|
|
|
|
if (diff < 60000) { // Less than 1 minute
|
|
return 'Just now';
|
|
} else if (diff < 3600000) { // Less than 1 hour
|
|
const minutes = Math.floor(diff / 60000);
|
|
return `${minutes}m ago`;
|
|
} else if (diff < 86400000) { // Less than 1 day
|
|
const hours = Math.floor(diff / 3600000);
|
|
const minutes = Math.floor((diff % 3600000) / 60000);
|
|
return `${hours}h ${minutes}m ago`;
|
|
} else { // More than 1 day
|
|
const days = Math.floor(diff / 86400000);
|
|
const hours = Math.floor((diff % 86400000) / 3600000);
|
|
return `${days}d ${hours}h ago`;
|
|
}
|
|
}
|
|
|
|
// Format flash size in human readable format
|
|
formatFlashSize(bytes) {
|
|
if (!bytes || bytes === 0) return 'Unknown';
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let size = bytes;
|
|
let unitIndex = 0;
|
|
|
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
size /= 1024;
|
|
unitIndex++;
|
|
}
|
|
|
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
}
|
|
|
|
// Clean up resources
|
|
cleanup() {
|
|
if (this.drawer) {
|
|
this.drawer.cleanup();
|
|
}
|
|
super.cleanup();
|
|
}
|
|
|
|
// Setup click event listeners for node cards
|
|
setupNodeCardClickListeners(container) {
|
|
const nodeCards = container.querySelectorAll('.node-card');
|
|
nodeCards.forEach(card => {
|
|
const nodeIp = card.dataset.nodeIp;
|
|
if (nodeIp) {
|
|
// Find the node data
|
|
const nodeResources = this.viewModel.get('nodeResources');
|
|
const nodeData = nodeResources.get(nodeIp);
|
|
|
|
if (nodeData) {
|
|
this.addEventListener(card, 'click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.openDrawerForNode(nodeData);
|
|
});
|
|
|
|
// Add hover cursor style
|
|
card.style.cursor = 'pointer';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
render() {
|
|
logger.debug('MonitoringViewComponent: Rendering...');
|
|
|
|
const isLoading = this.viewModel.get('isLoading');
|
|
const error = this.viewModel.get('error');
|
|
const clusterSummary = this.viewModel.get('clusterSummary');
|
|
const nodeResources = this.viewModel.get('nodeResources');
|
|
const lastUpdated = this.viewModel.get('lastUpdated');
|
|
|
|
// Render cluster summary
|
|
this.renderClusterSummary(isLoading, error, clusterSummary, lastUpdated);
|
|
|
|
// Render nodes monitoring
|
|
this.renderNodesMonitoring(isLoading, error, nodeResources);
|
|
}
|
|
|
|
renderClusterSummary(isLoading, error, clusterSummary, lastUpdated) {
|
|
const container = this.findElement('#cluster-summary');
|
|
if (!container) return;
|
|
|
|
if (isLoading) {
|
|
container.innerHTML = `
|
|
<div class="loading">
|
|
<div>Loading cluster resource summary...</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (error) {
|
|
container.innerHTML = `
|
|
<div class="error">
|
|
<div>❌ Error: ${error}</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const cpuUtilization = this.viewModel.getResourceUtilization('Cpu');
|
|
const memoryUtilization = this.viewModel.getResourceUtilization('Memory');
|
|
const storageUtilization = this.viewModel.getResourceUtilization('Storage');
|
|
|
|
const lastUpdatedText = lastUpdated ?
|
|
`Last updated: ${lastUpdated.toLocaleTimeString()}` :
|
|
'Never updated';
|
|
|
|
container.innerHTML = `
|
|
<div class="cluster-summary-content">
|
|
<div class="summary-header">
|
|
<h3>Summary</h3>
|
|
<div class="last-updated">${lastUpdatedText}</div>
|
|
</div>
|
|
|
|
<div class="summary-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-icon">🖥️</div>
|
|
<div class="stat-content">
|
|
<div class="stat-label">Total Nodes</div>
|
|
<div class="stat-value">${clusterSummary.totalNodes}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">⚡</div>
|
|
<div class="stat-content">
|
|
<div class="stat-label">CPU</div>
|
|
<div class="stat-value">${Math.round(clusterSummary.totalCpu - clusterSummary.availableCpu)}MHz / ${Math.round(clusterSummary.totalCpu)}MHz</div>
|
|
<div class="stat-utilization">
|
|
<div class="utilization-bar">
|
|
<div class="utilization-fill ${this.getUtilizationColorClass(cpuUtilization)}" style="width: ${cpuUtilization}%"></div>
|
|
</div>
|
|
<span class="utilization-text">${cpuUtilization}% used</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">🧠</div>
|
|
<div class="stat-content">
|
|
<div class="stat-label">Memory</div>
|
|
<div class="stat-value">${this.viewModel.formatResourceValue(clusterSummary.totalMemory - clusterSummary.availableMemory, 'memory')} / ${this.viewModel.formatResourceValue(clusterSummary.totalMemory, 'memory')}</div>
|
|
<div class="stat-utilization">
|
|
<div class="utilization-bar">
|
|
<div class="utilization-fill ${this.getUtilizationColorClass(memoryUtilization)}" style="width: ${memoryUtilization}%"></div>
|
|
</div>
|
|
<span class="utilization-text">${memoryUtilization}% used</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">💾</div>
|
|
<div class="stat-content">
|
|
<div class="stat-label">Storage</div>
|
|
<div class="stat-value">${this.viewModel.formatResourceValue(clusterSummary.totalStorage - clusterSummary.availableStorage, 'storage')} / ${this.viewModel.formatResourceValue(clusterSummary.totalStorage, 'storage')}</div>
|
|
<div class="stat-utilization">
|
|
<div class="utilization-bar">
|
|
<div class="utilization-fill ${this.getUtilizationColorClass(storageUtilization)}" style="width: ${storageUtilization}%"></div>
|
|
</div>
|
|
<span class="utilization-text">${storageUtilization}% used</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderNodesMonitoring(isLoading, error, nodeResources) {
|
|
const container = this.findElement('#nodes-monitoring');
|
|
if (!container) return;
|
|
|
|
if (isLoading) {
|
|
container.innerHTML = `
|
|
<div class="loading">
|
|
<div>Loading node resource data...</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (error) {
|
|
container.innerHTML = `
|
|
<div class="error">
|
|
<div>❌ Error: ${error}</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (!nodeResources || nodeResources.size === 0) {
|
|
container.innerHTML = `
|
|
<div class="no-data">
|
|
<div>No node resource data available</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const nodesHtml = Array.from(nodeResources.values()).map(nodeData => {
|
|
return this.renderNodeCard(nodeData);
|
|
}).join('');
|
|
|
|
container.innerHTML = `
|
|
<div class="nodes-monitoring-content">
|
|
<h3>🖥️ Node Resource Details</h3>
|
|
<div class="nodes-grid">
|
|
${nodesHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add click event listeners to node cards
|
|
this.setupNodeCardClickListeners(container);
|
|
}
|
|
|
|
renderNodeCard(nodeData) {
|
|
const { ip, hostname, resources, hasResources, error, resourceSource, labels } = nodeData;
|
|
|
|
|
|
if (!hasResources) {
|
|
return `
|
|
<div class="node-card error" data-node-ip="${ip}">
|
|
<div class="node-header">
|
|
<div class="node-title">${hostname || ip}</div>
|
|
<div class="node-ip">${ip}</div>
|
|
</div>
|
|
<div class="node-status">
|
|
<span class="status-badge error">❌ No Resources</span>
|
|
</div>
|
|
|
|
${labels && Object.keys(labels).length > 0 ? `
|
|
<div class="node-labels">
|
|
<div class="labels-container">
|
|
${Object.entries(labels).map(([key, value]) =>
|
|
`<span class="label-chip">${key}: ${value}</span>`
|
|
).join('')}
|
|
</div>
|
|
<div class="labels-divider"></div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="node-error">
|
|
<div class="error-label">⚠️ Error</div>
|
|
<div class="error-message">${error || 'Monitoring endpoint not available'}</div>
|
|
</div>
|
|
${nodeData.lastSeen ? `
|
|
<div class="node-uptime">
|
|
<div class="uptime-label">⏱️ Last Seen</div>
|
|
<div class="uptime-value">${this.formatLastSeen(nodeData.lastSeen)}</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Extract resource data (handle both monitoring API and basic data formats)
|
|
const cpu = resources?.cpu || {};
|
|
const memory = resources?.memory || {};
|
|
const storage = resources?.filesystem || resources?.storage || {};
|
|
const system = resources?.system || {};
|
|
|
|
let cpuTotal, cpuAvailable, cpuUsed, cpuUtilization;
|
|
let memoryTotal, memoryAvailable, memoryUsed, memoryUtilization;
|
|
let storageTotal, storageAvailable, storageUsed, storageUtilization;
|
|
|
|
if (resourceSource === 'monitoring') {
|
|
// Real monitoring API format
|
|
const cpuFreqMHz = nodeData.basic?.cpuFreqMHz || 80; // Get CPU frequency from basic resources
|
|
cpuTotal = cpuFreqMHz; // Total CPU frequency in MHz
|
|
cpuAvailable = cpuFreqMHz * (100 - (cpu.average_usage || 0)) / 100; // Available frequency
|
|
cpuUsed = cpuFreqMHz * (cpu.average_usage || 0) / 100; // Used frequency
|
|
cpuUtilization = Math.round(cpu.average_usage || 0);
|
|
|
|
memoryTotal = memory.total_heap || 0;
|
|
memoryAvailable = memory.free_heap || 0;
|
|
memoryUsed = memoryTotal - memoryAvailable;
|
|
memoryUtilization = memoryTotal > 0 ? Math.round((memoryUsed / memoryTotal) * 100) : 0;
|
|
|
|
storageTotal = storage.total_bytes || 0;
|
|
storageAvailable = storage.free_bytes || 0;
|
|
storageUsed = storageTotal - storageAvailable;
|
|
storageUtilization = storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0;
|
|
} else {
|
|
// Basic data format - use CPU frequency from basic resources
|
|
const cpuFreqMHz = nodeData.basic?.cpuFreqMHz || 80;
|
|
cpuTotal = cpuFreqMHz; // Total CPU frequency in MHz
|
|
cpuAvailable = cpuFreqMHz * (cpu.available || 0.8); // Available frequency
|
|
cpuUsed = cpuFreqMHz * (cpu.used || 0.2); // Used frequency
|
|
cpuUtilization = cpuTotal > 0 ? Math.round((cpuUsed / cpuTotal) * 100) : 0;
|
|
|
|
memoryTotal = memory.total || 0;
|
|
memoryAvailable = memory.available || memory.free || 0;
|
|
memoryUsed = memoryTotal - memoryAvailable;
|
|
memoryUtilization = memoryTotal > 0 ? Math.round((memoryUsed / memoryTotal) * 100) : 0;
|
|
|
|
storageTotal = storage.total || 0;
|
|
storageAvailable = storage.available || storage.free || 0;
|
|
storageUsed = storageTotal - storageAvailable;
|
|
storageUtilization = storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0;
|
|
}
|
|
|
|
const resourceSourceText = resourceSource === 'monitoring' ? '📊 Full Monitoring' :
|
|
resourceSource === 'basic' ? '📋 Basic Data' : '❓ Unknown';
|
|
|
|
return `
|
|
<div class="node-card" data-node-ip="${ip}">
|
|
<div class="node-header">
|
|
<div class="node-title">${hostname || ip}</div>
|
|
<div class="node-ip">${ip}</div>
|
|
</div>
|
|
<div class="node-status">
|
|
<span class="status-badge ${resourceSource === 'monitoring' ? 'success' : 'warning'}">${resourceSourceText}</span>
|
|
</div>
|
|
|
|
${labels && Object.keys(labels).length > 0 ? `
|
|
<div class="node-labels">
|
|
<div class="labels-container">
|
|
${Object.entries(labels).map(([key, value]) =>
|
|
`<span class="label-chip">${key}: ${value}</span>`
|
|
).join('')}
|
|
</div>
|
|
<div class="labels-divider"></div>
|
|
</div>
|
|
` : ''}
|
|
|
|
${system.uptime_formatted ? `
|
|
<div class="node-uptime">
|
|
<div class="uptime-label">⏱️ Uptime</div>
|
|
<div class="uptime-value">${system.uptime_formatted}</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="node-latency">
|
|
<div class="latency-label">🐢 Latency</div>
|
|
<div class="latency-value">${nodeData.latency ? `${nodeData.latency}ms` : 'N/A'}</div>
|
|
</div>
|
|
<div class="latency-divider"></div>
|
|
|
|
${(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize) ? `
|
|
<div class="node-flash">
|
|
<div class="flash-label">💾 Flash</div>
|
|
<div class="flash-value">${this.formatFlashSize(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize)}</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="node-resources">
|
|
<div class="resource-item">
|
|
<div class="resource-label">⚡ CPU</div>
|
|
<div class="resource-value">
|
|
<span class="value-label">Used:</span> ${Math.round(cpuUsed)}MHz / <span class="value-label">Total:</span> ${Math.round(cpuTotal)}MHz
|
|
</div>
|
|
<div class="resource-utilization">
|
|
<div class="utilization-bar">
|
|
<div class="utilization-fill ${this.getUtilizationColorClass(cpuUtilization)}" style="width: ${cpuUtilization}%"></div>
|
|
</div>
|
|
<span class="utilization-text">${cpuUtilization}% used</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resource-item">
|
|
<div class="resource-label">🧠 Memory</div>
|
|
<div class="resource-value">
|
|
<span class="value-label">Used:</span> ${this.viewModel.formatResourceValue(memoryUsed, 'memory')} / <span class="value-label">Total:</span> ${this.viewModel.formatResourceValue(memoryTotal, 'memory')}
|
|
</div>
|
|
<div class="resource-utilization">
|
|
<div class="utilization-bar">
|
|
<div class="utilization-fill ${this.getUtilizationColorClass(memoryUtilization)}" style="width: ${memoryUtilization}%"></div>
|
|
</div>
|
|
<span class="utilization-text">${memoryUtilization}% used</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resource-item">
|
|
<div class="resource-label">💾 Storage</div>
|
|
<div class="resource-value">
|
|
<span class="value-label">Used:</span> ${this.viewModel.formatResourceValue(storageUsed, 'storage')} / <span class="value-label">Total:</span> ${this.viewModel.formatResourceValue(storageTotal, 'storage')}
|
|
</div>
|
|
<div class="resource-utilization">
|
|
<div class="utilization-bar">
|
|
<div class="utilization-fill ${this.getUtilizationColorClass(storageUtilization)}" style="width: ${storageUtilization}%"></div>
|
|
</div>
|
|
<span class="utilization-text">${storageUtilization}% used</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|