feat: monitoring view

This commit is contained in:
2025-09-20 12:49:44 +02:00
parent e0e86f88a9
commit c13d544e54
5 changed files with 920 additions and 3 deletions

View File

@@ -0,0 +1,346 @@
// 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;
}
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();
}
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" 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" 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" 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>
`;
}
renderNodeCard(nodeData) {
const { ip, hostname, resources, hasResources, error, resourceSource } = nodeData;
if (!hasResources) {
return `
<div class="node-card error">
<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>
<div class="node-error">
${error || 'Monitoring endpoint not available'}
</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 || {};
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">
<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>
<div class="node-resources">
<div class="resource-item">
<div class="resource-label">⚡ CPU</div>
<div class="resource-value">${Math.round(cpuUsed)}MHz / ${Math.round(cpuTotal)}MHz</div>
<div class="resource-utilization">
<div class="utilization-bar">
<div class="utilization-fill" 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">${this.viewModel.formatResourceValue(memoryUsed, 'memory')} / ${this.viewModel.formatResourceValue(memoryTotal, 'memory')}</div>
<div class="resource-utilization">
<div class="utilization-bar">
<div class="utilization-fill" 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">${this.viewModel.formatResourceValue(storageUsed, 'storage')} / ${this.viewModel.formatResourceValue(storageTotal, 'storage')}</div>
<div class="resource-utilization">
<div class="utilization-bar">
<div class="utilization-fill" style="width: ${storageUtilization}%"></div>
</div>
<span class="utilization-text">${storageUtilization}% used</span>
</div>
</div>
</div>
</div>
`;
}
}