|
@@ -68,6 +68,9 @@ async function selectVM(vmName) {
|
|
|
selectedVM = vmName;
|
|
selectedVM = vmName;
|
|
|
loadVMs(); // Refresh to highlight selected
|
|
loadVMs(); // Refresh to highlight selected
|
|
|
|
|
|
|
|
|
|
+ // Clean up previous charts
|
|
|
|
|
+ cleanupCharts();
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const response = await fetch(`${API_BASE}/vms/${vmName}/info`);
|
|
const response = await fetch(`${API_BASE}/vms/${vmName}/info`);
|
|
|
const vm = await response.json();
|
|
const vm = await response.json();
|
|
@@ -78,6 +81,11 @@ async function selectVM(vmName) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
displayVMDetails(vm, vmName);
|
|
displayVMDetails(vm, vmName);
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize performance charts only if VM is running
|
|
|
|
|
+ if (vm.state === 'running') {
|
|
|
|
|
+ setTimeout(() => initializePerformanceCharts(vmName), 100);
|
|
|
|
|
+ }
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Error loading VM info:', error);
|
|
console.error('Error loading VM info:', error);
|
|
|
showError('Error al cargar información de la VM');
|
|
showError('Error al cargar información de la VM');
|
|
@@ -188,6 +196,29 @@ function displayVMDetails(vm, vmName) {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Performance Charts -->
|
|
|
|
|
+ ${vm.state === 'running' ? `
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <h6 class="card-title mb-0"><i class="bi bi-graph-up"></i> Rendimiento</h6>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <div class="row">
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <div style="position: relative; height: 250px;">
|
|
|
|
|
+ <canvas id="cpuChart"></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <div style="position: relative; height: 250px;">
|
|
|
|
|
+ <canvas id="memoryChart"></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ` : ''}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="actions-group">
|
|
<div class="actions-group">
|
|
@@ -957,3 +988,254 @@ async function deleteSnapshot(vmName, snapshotName) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Chart instances storage
|
|
|
|
|
+let cpuChart = null;
|
|
|
|
|
+let memoryChart = null;
|
|
|
|
|
+let statsInterval = null;
|
|
|
|
|
+const statsHistory = {
|
|
|
|
|
+ cpu: [],
|
|
|
|
|
+ memory: [],
|
|
|
|
|
+ maxPoints: 20
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Initialize and update performance charts
|
|
|
|
|
+async function initializePerformanceCharts(vmName) {
|
|
|
|
|
+ // Clear any existing interval
|
|
|
|
|
+ if (statsInterval) clearInterval(statsInterval);
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize charts immediately
|
|
|
|
|
+ await updatePerformanceStats(vmName);
|
|
|
|
|
+
|
|
|
|
|
+ // Update every 2 seconds
|
|
|
|
|
+ statsInterval = setInterval(() => updatePerformanceStats(vmName), 2000);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Update performance statistics
|
|
|
|
|
+async function updatePerformanceStats(vmName) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}/stats`);
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (data.error || data.state === 'error') {
|
|
|
|
|
+ console.log('VM not running or stats not available');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const timestamp = new Date().toLocaleTimeString('es-ES', {
|
|
|
|
|
+ hour: '2-digit',
|
|
|
|
|
+ minute: '2-digit',
|
|
|
|
|
+ second: '2-digit'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Update history
|
|
|
|
|
+ statsHistory.cpu.push({
|
|
|
|
|
+ time: timestamp,
|
|
|
|
|
+ value: data.cpu_percent
|
|
|
|
|
+ });
|
|
|
|
|
+ statsHistory.memory.push({
|
|
|
|
|
+ time: timestamp,
|
|
|
|
|
+ value: data.memory_percent
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Keep only last maxPoints
|
|
|
|
|
+ if (statsHistory.cpu.length > statsHistory.maxPoints) {
|
|
|
|
|
+ statsHistory.cpu.shift();
|
|
|
|
|
+ statsHistory.memory.shift();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update CPU Chart
|
|
|
|
|
+ if (cpuChart) {
|
|
|
|
|
+ cpuChart.data.labels = statsHistory.cpu.map(s => s.time);
|
|
|
|
|
+ cpuChart.data.datasets[0].data = statsHistory.cpu.map(s => s.value);
|
|
|
|
|
+ cpuChart.update('none');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ createCPUChart();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update Memory Chart
|
|
|
|
|
+ if (memoryChart) {
|
|
|
|
|
+ memoryChart.data.labels = statsHistory.memory.map(s => s.time);
|
|
|
|
|
+ memoryChart.data.datasets[0].data = statsHistory.memory.map(s => s.value);
|
|
|
|
|
+ memoryChart.update('none');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ createMemoryChart();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error updating stats:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Create CPU chart
|
|
|
|
|
+function createCPUChart() {
|
|
|
|
|
+ const ctx = document.getElementById('cpuChart');
|
|
|
|
|
+ if (!ctx) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Destroy existing chart if any
|
|
|
|
|
+ if (cpuChart) cpuChart.destroy();
|
|
|
|
|
+
|
|
|
|
|
+ cpuChart = new Chart(ctx, {
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ labels: statsHistory.cpu.map(s => s.time),
|
|
|
|
|
+ datasets: [{
|
|
|
|
|
+ label: 'CPU (%)',
|
|
|
|
|
+ data: statsHistory.cpu.map(s => s.value),
|
|
|
|
|
+ borderColor: '#667eea',
|
|
|
|
|
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
|
|
|
|
+ tension: 0.4,
|
|
|
|
|
+ fill: true,
|
|
|
|
|
+ borderWidth: 2,
|
|
|
|
|
+ pointRadius: 4,
|
|
|
|
|
+ pointBackgroundColor: '#667eea',
|
|
|
|
|
+ pointBorderColor: '#fff',
|
|
|
|
|
+ pointBorderWidth: 2,
|
|
|
|
|
+ pointHoverRadius: 6
|
|
|
|
|
+ }],
|
|
|
|
|
+ },
|
|
|
|
|
+ options: {
|
|
|
|
|
+ responsive: true,
|
|
|
|
|
+ maintainAspectRatio: false,
|
|
|
|
|
+ plugins: {
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ display: true,
|
|
|
|
|
+ labels: {
|
|
|
|
|
+ font: { size: 12 },
|
|
|
|
|
+ padding: 15,
|
|
|
|
|
+ color: '#666'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.8)',
|
|
|
|
|
+ padding: 12,
|
|
|
|
|
+ titleFont: { size: 12 },
|
|
|
|
|
+ bodyFont: { size: 12 },
|
|
|
|
|
+ callbacks: {
|
|
|
|
|
+ label: function(context) {
|
|
|
|
|
+ return context.dataset.label + ': ' + context.parsed.y.toFixed(2) + '%';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ scales: {
|
|
|
|
|
+ y: {
|
|
|
|
|
+ beginAtZero: true,
|
|
|
|
|
+ max: 100,
|
|
|
|
|
+ ticks: {
|
|
|
|
|
+ stepSize: 25,
|
|
|
|
|
+ callback: function(value) {
|
|
|
|
|
+ return value + '%';
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ color: 'rgba(0,0,0,0.05)'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ x: {
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ color: 'rgba(0,0,0,0.05)'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Create Memory chart
|
|
|
|
|
+function createMemoryChart() {
|
|
|
|
|
+ const ctx = document.getElementById('memoryChart');
|
|
|
|
|
+ if (!ctx) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Destroy existing chart if any
|
|
|
|
|
+ if (memoryChart) memoryChart.destroy();
|
|
|
|
|
+
|
|
|
|
|
+ memoryChart = new Chart(ctx, {
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ labels: statsHistory.memory.map(s => s.time),
|
|
|
|
|
+ datasets: [{
|
|
|
|
|
+ label: 'Memoria (%)',
|
|
|
|
|
+ data: statsHistory.memory.map(s => s.value),
|
|
|
|
|
+ borderColor: '#764ba2',
|
|
|
|
|
+ backgroundColor: 'rgba(118, 75, 162, 0.1)',
|
|
|
|
|
+ tension: 0.4,
|
|
|
|
|
+ fill: true,
|
|
|
|
|
+ borderWidth: 2,
|
|
|
|
|
+ pointRadius: 4,
|
|
|
|
|
+ pointBackgroundColor: '#764ba2',
|
|
|
|
|
+ pointBorderColor: '#fff',
|
|
|
|
|
+ pointBorderWidth: 2,
|
|
|
|
|
+ pointHoverRadius: 6
|
|
|
|
|
+ }],
|
|
|
|
|
+ },
|
|
|
|
|
+ options: {
|
|
|
|
|
+ responsive: true,
|
|
|
|
|
+ maintainAspectRatio: false,
|
|
|
|
|
+ plugins: {
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ display: true,
|
|
|
|
|
+ labels: {
|
|
|
|
|
+ font: { size: 12 },
|
|
|
|
|
+ padding: 15,
|
|
|
|
|
+ color: '#666'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.8)',
|
|
|
|
|
+ padding: 12,
|
|
|
|
|
+ titleFont: { size: 12 },
|
|
|
|
|
+ bodyFont: { size: 12 },
|
|
|
|
|
+ callbacks: {
|
|
|
|
|
+ label: function(context) {
|
|
|
|
|
+ return context.dataset.label + ': ' + context.parsed.y.toFixed(2) + '%';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ scales: {
|
|
|
|
|
+ y: {
|
|
|
|
|
+ beginAtZero: true,
|
|
|
|
|
+ max: 100,
|
|
|
|
|
+ ticks: {
|
|
|
|
|
+ stepSize: 25,
|
|
|
|
|
+ callback: function(value) {
|
|
|
|
|
+ return value + '%';
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ color: 'rgba(0,0,0,0.05)'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ x: {
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ color: 'rgba(0,0,0,0.05)'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Clean up charts when switching VMs
|
|
|
|
|
+function cleanupCharts() {
|
|
|
|
|
+ if (statsInterval) {
|
|
|
|
|
+ clearInterval(statsInterval);
|
|
|
|
|
+ statsInterval = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (cpuChart) {
|
|
|
|
|
+ cpuChart.destroy();
|
|
|
|
|
+ cpuChart = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (memoryChart) {
|
|
|
|
|
+ memoryChart.destroy();
|
|
|
|
|
+ memoryChart = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Reset history
|
|
|
|
|
+ statsHistory.cpu = [];
|
|
|
|
|
+ statsHistory.memory = [];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|