Some checks failed
CI / ci (push) Has been cancelled
- Expanded mock data to include additional host labels and updated CPU current and projected values for better representation. - Modified JavaScript to conditionally display projected CPU statistics and standard deviation, improving user experience. - Refactored chart configuration to dynamically handle datasets based on the presence of projected data. - Updated HTML to include a new block for displaying standard deviation, enhancing clarity in CPU metrics presentation.
323 lines
19 KiB
JavaScript
323 lines
19 KiB
JavaScript
/**
|
|
* Dashboard logic: stats rendering, audit selector, CPU chart, migration table.
|
|
* Expects globals: SKELETON_MODE, CURRENT_CLUSTER, auditData (set by index.html).
|
|
* Depends on: utils.js (formatBytes, getCSSVar, calculateStats)
|
|
*/
|
|
(function() {
|
|
var cpuDistributionChart = null;
|
|
|
|
document.getElementById('auditSelector').addEventListener('change', function(e) {
|
|
var option = this.options[this.selectedIndex];
|
|
if (!option) return;
|
|
document.getElementById('previewCpu').textContent = option.dataset.cpu || '1.0';
|
|
document.getElementById('previewRam').textContent = option.dataset.ram || '1.0';
|
|
document.getElementById('previewScope').textContent = option.dataset.scope || 'Full Cluster';
|
|
document.getElementById('previewStrategy').textContent = option.dataset.strategy || 'Balanced';
|
|
});
|
|
|
|
function setStat(key, text) {
|
|
document.querySelectorAll('[data-stats="' + key + '"]').forEach(function(el) {
|
|
el.textContent = text;
|
|
el.classList.remove('animate-pulse');
|
|
});
|
|
}
|
|
function setProgress(key, value) {
|
|
document.querySelectorAll('[data-stats="' + key + '"]').forEach(function(el) {
|
|
if (el.tagName === 'PROGRESS') {
|
|
el.value = value;
|
|
el.classList.remove('animate-pulse');
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderStats(data) {
|
|
if (!data) return;
|
|
var el = function(k) { return document.querySelector('[data-stats="' + k + '"]'); };
|
|
var regionBadge = document.getElementById('regionBadge');
|
|
if (regionBadge) regionBadge.textContent = data.region && data.region.name ? data.region.name : '—';
|
|
setStat('pcpu.usage', Number((data.pcpu && data.pcpu.usage) || 0).toFixed(1));
|
|
setStat('pcpu.total', String((data.pcpu && data.pcpu.total) || 0));
|
|
setStat('pcpu.used_percentage', Number((data.pcpu && data.pcpu.used_percentage) || 0).toFixed(1) + '%');
|
|
setStat('pcpu.usage_val', Number((data.pcpu && data.pcpu.usage) || 0).toFixed(1) + ' CPU');
|
|
setProgress('pcpu.progress', (data.pcpu && data.pcpu.used_percentage) || 0);
|
|
setStat('pcpu.free', String((data.pcpu && data.pcpu.free) || 0));
|
|
var pramUsageGb = formatBytes(data.pram && data.pram.usage, 'GB');
|
|
var pramTotalGb = formatBytes(data.pram && data.pram.total, 'GB');
|
|
var pramFreeGb = formatBytes(data.pram && data.pram.free, 'GB');
|
|
setStat('pram.usage_gb', pramUsageGb);
|
|
setStat('pram.total_gb', pramTotalGb);
|
|
setStat('pram.used_percentage', Number((data.pram && data.pram.used_percentage) || 0).toFixed(1) + '%');
|
|
setStat('pram.usage_gb_val', pramUsageGb + ' GB');
|
|
setProgress('pram.progress', (data.pram && data.pram.used_percentage) || 0);
|
|
setStat('pram.free_gb', pramFreeGb + ' GB');
|
|
setStat('vm.active', String(data.vm && data.vm.active));
|
|
setStat('vm.stopped', String(data.vm && data.vm.stopped));
|
|
setStat('vm.count', String(data.vm && data.vm.count));
|
|
setStat('flavors.first_name', data.flavors && data.flavors.first_common_flavor ? data.flavors.first_common_flavor.name : '—');
|
|
setStat('vm.avg_cpu', Number((data.vm && data.vm.avg_cpu) || 0).toFixed(1));
|
|
setStat('vm.density', Number((data.vm && data.vm.density) || 0).toFixed(1) + '/host');
|
|
setStat('vcpu.allocated_total', ((data.vcpu && data.vcpu.allocated) || 0) + ' / ' + ((data.vcpu && data.vcpu.total) || 0) + ' vCPU');
|
|
setProgress('vcpu.progress', (data.vcpu && data.vcpu.allocated_percentage) || 0);
|
|
setStat('vcpu.allocated_percentage', Number((data.vcpu && data.vcpu.allocated_percentage) || 0).toFixed(1) + '%');
|
|
var vcpuOver = el('vcpu.overcommit');
|
|
if (vcpuOver) {
|
|
vcpuOver.textContent = 'overcommit: ' + Number((data.vcpu && data.vcpu.overcommit_ratio) || 0).toFixed(1) + ' / ' + Number((data.vcpu && data.vcpu.overcommit_max) || 0).toFixed(1) + ' — ' + Number((data.vcpu && data.vcpu.allocated_percentage) || 0).toFixed(1) + '% allocated';
|
|
vcpuOver.classList.remove('animate-pulse');
|
|
}
|
|
var vramAllocGb = formatBytes(data.vram && data.vram.allocated, 'GB');
|
|
var vramTotalGb = formatBytes(data.vram && data.vram.total, 'GB');
|
|
setStat('vram.allocated_total', vramAllocGb + ' / ' + vramTotalGb + ' GB');
|
|
setProgress('vram.progress', (data.vram && data.vram.allocated_percentage) || 0);
|
|
setStat('vram.allocated_percentage', Number((data.vram && data.vram.allocated_percentage) || 0).toFixed(1) + '%');
|
|
var vramOver = el('vram.overcommit');
|
|
if (vramOver) {
|
|
vramOver.textContent = 'overcommit: ' + Number((data.vram && data.vram.overcommit_ratio) || 0).toFixed(1) + ' / ' + Number((data.vram && data.vram.overcommit_max) || 0).toFixed(1) + ' — ' + Number((data.vram && data.vram.allocated_percentage) || 0).toFixed(1) + '% allocated';
|
|
vramOver.classList.remove('animate-pulse');
|
|
}
|
|
setStat('flavors.first_count', (data.flavors && data.flavors.first_common_flavor ? data.flavors.first_common_flavor.count : 0) + ' instances');
|
|
var vmCount = data.vm && data.vm.count ? data.vm.count : 0;
|
|
var firstCount = data.flavors && data.flavors.first_common_flavor ? data.flavors.first_common_flavor.count : 0;
|
|
setStat('flavors.first_share', (vmCount ? Math.round(firstCount / vmCount * 100) : 0) + '%');
|
|
setStat('flavors.second_name', data.flavors && data.flavors.second_common_flavor ? data.flavors.second_common_flavor.name : '—');
|
|
setStat('flavors.second_count', data.flavors && data.flavors.second_common_flavor ? String(data.flavors.second_common_flavor.count) : '—');
|
|
setStat('flavors.third_name', data.flavors && data.flavors.third_common_flavor ? data.flavors.third_common_flavor.name : '—');
|
|
setStat('flavors.third_count', data.flavors && data.flavors.third_common_flavor ? String(data.flavors.third_common_flavor.count) : '—');
|
|
document.querySelectorAll('[data-stats]').forEach(function(n) { n.classList.remove('animate-pulse'); });
|
|
}
|
|
|
|
function renderAudits(auditsList) {
|
|
if (!auditsList || !auditsList.length) {
|
|
var countEl = document.getElementById('auditsCount');
|
|
if (countEl) countEl.textContent = '0 available';
|
|
var sel = document.getElementById('auditSelector');
|
|
if (sel) { sel.disabled = false; sel.innerHTML = '<option value="">No audits</option>'; }
|
|
return;
|
|
}
|
|
window.auditData = {};
|
|
auditsList.forEach(function(a) {
|
|
window.auditData[a.id] = {
|
|
name: a.name,
|
|
migrations: typeof a.migrations === 'string' ? JSON.parse(a.migrations) : a.migrations,
|
|
hostData: {
|
|
labels: typeof a.host_labels === 'string' ? JSON.parse(a.host_labels) : a.host_labels,
|
|
current: typeof a.cpu_current === 'string' ? JSON.parse(a.cpu_current) : a.cpu_current,
|
|
projected: typeof a.cpu_projected === 'string' ? JSON.parse(a.cpu_projected) : a.cpu_projected
|
|
}
|
|
};
|
|
});
|
|
var sel = document.getElementById('auditSelector');
|
|
if (sel) {
|
|
sel.disabled = false;
|
|
sel.innerHTML = '';
|
|
auditsList.forEach(function(audit) {
|
|
var opt = document.createElement('option');
|
|
opt.value = audit.id;
|
|
opt.setAttribute('data-cpu', audit.cpu_weight || '1.0');
|
|
opt.setAttribute('data-ram', audit.ram_weight || '1.0');
|
|
opt.setAttribute('data-scope', audit.scope || 'Full Cluster');
|
|
opt.setAttribute('data-strategy', audit.strategy || 'Balanced');
|
|
opt.setAttribute('data-goal', audit.goal || '');
|
|
var dateStr = audit.created_at ? new Date(audit.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '';
|
|
opt.textContent = audit.name + ' (' + dateStr + ')';
|
|
sel.appendChild(opt);
|
|
});
|
|
}
|
|
var countEl = document.getElementById('auditsCount');
|
|
if (countEl) countEl.textContent = auditsList.length + ' available';
|
|
if (auditsList.length > 0) {
|
|
document.getElementById('auditSelector').dispatchEvent(new Event('change'));
|
|
loadSelectedAudit();
|
|
}
|
|
}
|
|
|
|
window.loadSelectedAudit = function() {
|
|
var auditId = document.getElementById('auditSelector').value;
|
|
updateMigrationTable(auditId);
|
|
updateCPUCharts(auditId);
|
|
};
|
|
|
|
function updateMigrationTable(auditId) {
|
|
var tbody = document.getElementById('migrationTableBody');
|
|
var migrationCount = document.getElementById('migrationCount');
|
|
var data = window.auditData && window.auditData[auditId];
|
|
|
|
if (!data || !data.migrations || data.migrations.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-6 text-base-content/60">No migration actions recommended</td></tr>';
|
|
migrationCount.textContent = '0 actions';
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
data.migrations.forEach(function(migration) {
|
|
var impact = migration.impact || 'Low';
|
|
var impactClass = { 'Low': 'badge-success', 'Medium': 'badge-warning', 'High': 'badge-error' }[impact] || 'badge-neutral';
|
|
html += '<tr><td class="font-medium"><div>' + migration.instanceName + '</div></td><td><div class="flex items-center gap-2"><span class="badge badge-outline badge-xs">' + migration.source + '</span><svg class="w-3 h-3 text-base-content/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg><span class="badge badge-primary badge-outline badge-xs">' + migration.destination + '</span></div></td><td><code class="text-xs bg-base-200 px-2 py-1 rounded">' + migration.flavor + '</code></td><td><span class="badge ' + impactClass + ' badge-xs">' + impact + '</span></td></tr>';
|
|
});
|
|
tbody.innerHTML = html;
|
|
migrationCount.textContent = data.migrations.length + ' action' + (data.migrations.length !== 1 ? 's' : '');
|
|
}
|
|
|
|
function updateCPUCharts(auditId) {
|
|
var data = window.auditData && window.auditData[auditId];
|
|
if (!data || !data.hostData) return;
|
|
|
|
var hasProjected = (auditId !== 'current');
|
|
var ctx = document.getElementById('cpuDistributionChart').getContext('2d');
|
|
var currentStats = calculateStats(data.hostData.current);
|
|
|
|
document.getElementById('currentCpuMean').textContent = currentStats.mean.toFixed(1);
|
|
document.getElementById('currentCpuStd').textContent = (currentStats.std * 0.5).toFixed(1);
|
|
var stdBlock = document.getElementById('currentCpuStdBlock');
|
|
if (stdBlock) stdBlock.style.display = hasProjected ? '' : 'none';
|
|
|
|
if (cpuDistributionChart) cpuDistributionChart.destroy();
|
|
|
|
var colors = {
|
|
primary: getCSSVar('--color-primary'),
|
|
secondary: getCSSVar('--color-secondary'),
|
|
accent: getCSSVar('--color-accent'),
|
|
neutral: getCSSVar('--color-neutral'),
|
|
info: getCSSVar('--color-info'),
|
|
success: getCSSVar('--color-success'),
|
|
warning: getCSSVar('--color-warning'),
|
|
error: getCSSVar('--color-error')
|
|
};
|
|
var textColor = getCSSVar('--color-base-content');
|
|
var gridColor = getCSSVar('--chart-grid-color') || textColor;
|
|
|
|
var datasets = [
|
|
{ label: 'Current', data: data.hostData.current.slice(), backgroundColor: colors.info + '40', borderColor: colors.info, borderWidth: 1, borderRadius: 3 }
|
|
];
|
|
if (hasProjected) {
|
|
datasets.push({ label: 'Projected', data: data.hostData.projected.slice(), backgroundColor: colors.warning + '40', borderColor: colors.warning, borderWidth: 1, borderRadius: 3 });
|
|
}
|
|
|
|
var annotationConfig = {
|
|
MeanLine: { type: 'line', yMin: currentStats.mean, yMax: currentStats.mean, borderColor: colors.success, borderWidth: 2, borderDash: [] }
|
|
};
|
|
if (hasProjected) {
|
|
annotationConfig.upperStdLine = { type: 'line', yMin: currentStats.mean + currentStats.std * 0.5, yMax: currentStats.mean + currentStats.std * 0.5, borderColor: colors.error, borderWidth: 1, borderDash: [5, 5] };
|
|
annotationConfig.lowerStdLine = { type: 'line', yMin: currentStats.mean > currentStats.std * 0.5 ? currentStats.mean - currentStats.std * 0.5 : 0, yMax: currentStats.mean > currentStats.std * 0.5 ? currentStats.mean - currentStats.std * 0.5 : 0, borderColor: colors.error, borderWidth: 1, borderDash: [5, 5] };
|
|
}
|
|
|
|
cpuDistributionChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.hostData.labels,
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: {
|
|
onComplete: function() {
|
|
var chart = this.chart || this;
|
|
if (chart._hidingDataset === undefined) return;
|
|
var i = chart._hidingDataset;
|
|
chart.getDatasetMeta(i).hidden = true;
|
|
chart.data.datasets[i].data = chart._cpuOriginalData[i].slice();
|
|
delete chart._hidingDataset;
|
|
chart.update('none');
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
align: 'center',
|
|
onClick: function(e, legendItem, legend) {
|
|
var i = legendItem.datasetIndex;
|
|
var chart = legend.chart;
|
|
var len = chart.data.labels.length;
|
|
if (chart.isDatasetVisible(i)) {
|
|
chart._hidingDataset = i;
|
|
chart.data.datasets[i].data = Array(len).fill(0);
|
|
chart.update();
|
|
} else {
|
|
chart.data.datasets[i].data = Array(len).fill(0);
|
|
chart.show(i);
|
|
chart.update('none');
|
|
chart.data.datasets[i].data = chart._cpuOriginalData[i].slice();
|
|
chart.update();
|
|
}
|
|
},
|
|
labels: {
|
|
usePointStyle: true,
|
|
pointStyle: 'rect',
|
|
boxWidth: 14,
|
|
boxHeight: 14,
|
|
padding: 12,
|
|
color: textColor,
|
|
generateLabels: function(chart) {
|
|
var datasets = chart.data.datasets;
|
|
var labelColor = getCSSVar('--color-base-content') || textColor;
|
|
return datasets.map(function(ds, i) {
|
|
return { text: ds.label, fillStyle: ds.borderColor, strokeStyle: ds.borderColor, lineWidth: 1, fontColor: labelColor, color: labelColor, hidden: !chart.isDatasetVisible(i), datasetIndex: i };
|
|
});
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + Number(ctx.parsed.y).toFixed(2) + '% CPU'; } }
|
|
},
|
|
annotation: {
|
|
annotations: annotationConfig
|
|
}
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true, max: 100, grid: { drawBorder: false, color: gridColor }, ticks: { color: textColor, callback: function(value) { return value + '%'; } } },
|
|
x: { grid: { display: false }, ticks: { display: false }, barPercentage: 1, categoryPercentage: 0.85 }
|
|
}
|
|
}
|
|
});
|
|
cpuDistributionChart._cpuOriginalData = hasProjected
|
|
? [ data.hostData.current.slice(), data.hostData.projected.slice() ]
|
|
: [ data.hostData.current.slice() ];
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (typeof SKELETON_MODE !== 'undefined' && SKELETON_MODE) {
|
|
Promise.all([
|
|
fetch('/api/stats/').then(function(r) { return r.ok ? r.json() : Promise.reject(r); }),
|
|
fetch('/api/audits/').then(function(r) { return r.ok ? r.json() : Promise.reject(r); })
|
|
]).then(function(results) {
|
|
renderStats(results[0]);
|
|
renderAudits(results[1].audits);
|
|
if (!results[1].audits || results[1].audits.length === 0) {
|
|
var cc = results[1].current_cluster;
|
|
if (cc && cc.host_labels && cc.cpu_current && cc.host_labels.length) {
|
|
window.auditData = window.auditData || {};
|
|
window.auditData.current = { hostData: { labels: cc.host_labels, current: cc.cpu_current, projected: cc.cpu_current } };
|
|
updateCPUCharts('current');
|
|
}
|
|
}
|
|
}).catch(function(err) {
|
|
var msg = err.status ? 'Failed to load data (' + err.status + ')' : 'Failed to load data';
|
|
var countEl = document.getElementById('auditsCount');
|
|
if (countEl) countEl.textContent = msg;
|
|
fetch('/api/stats/').then(function(r) { return r.ok ? r.json() : null; }).then(function(d) { if (d) renderStats(d); });
|
|
fetch('/api/audits/').then(function(r) { return r.ok ? r.json() : null; }).then(function(d) { if (d && d.audits) renderAudits(d.audits); });
|
|
});
|
|
} else {
|
|
var initialAudit = typeof INITIAL_AUDIT_ID !== 'undefined' ? INITIAL_AUDIT_ID : '';
|
|
if (initialAudit && window.auditData && window.auditData[initialAudit]) {
|
|
document.getElementById('auditSelector').dispatchEvent(new Event('change'));
|
|
loadSelectedAudit();
|
|
} else if (!initialAudit && typeof CURRENT_CLUSTER !== 'undefined' && CURRENT_CLUSTER && CURRENT_CLUSTER.host_labels && CURRENT_CLUSTER.host_labels.length) {
|
|
window.auditData = window.auditData || {};
|
|
window.auditData.current = { hostData: { labels: CURRENT_CLUSTER.host_labels, current: CURRENT_CLUSTER.cpu_current, projected: CURRENT_CLUSTER.cpu_current } };
|
|
updateCPUCharts('current');
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('themechange', function() {
|
|
if (cpuDistributionChart) {
|
|
var auditId = document.getElementById('auditSelector').value;
|
|
cpuDistributionChart.destroy();
|
|
cpuDistributionChart = null;
|
|
if (auditId) updateCPUCharts(auditId);
|
|
}
|
|
});
|
|
})();
|