203 lines
6.7 KiB
JavaScript
203 lines
6.7 KiB
JavaScript
let currentMetric = 'cpu';
|
|
let clusterData = null;
|
|
|
|
const formatBytes = (bytes) => {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
const formatCpu = (millicores) => {
|
|
if (millicores === 0) return '0 m';
|
|
return `${millicores} m`;
|
|
};
|
|
|
|
document.getElementById('btn-cpu').addEventListener('click', (e) => {
|
|
document.getElementById('btn-cpu').classList.add('active');
|
|
document.getElementById('btn-mem').classList.remove('active');
|
|
currentMetric = 'cpu';
|
|
if (clusterData) renderChart(clusterData);
|
|
});
|
|
|
|
document.getElementById('btn-mem').addEventListener('click', (e) => {
|
|
document.getElementById('btn-mem').classList.add('active');
|
|
document.getElementById('btn-cpu').classList.remove('active');
|
|
currentMetric = 'memory';
|
|
if (clusterData) renderChart(clusterData);
|
|
});
|
|
|
|
async function fetchData() {
|
|
try {
|
|
const response = await fetch('/api/cluster-data');
|
|
if (!response.ok) throw new Error('Failed to fetch data');
|
|
clusterData = await response.json();
|
|
|
|
document.getElementById('loading').classList.add('hidden');
|
|
renderChart(clusterData);
|
|
} catch (error) {
|
|
console.error(error);
|
|
document.getElementById('loading').innerHTML = `<p style="color: #ef4444;">Error loading data: ${error.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function renderChart(data) {
|
|
const container = document.getElementById('chart-container');
|
|
const width = container.clientWidth;
|
|
const height = container.clientHeight;
|
|
|
|
// Clear previous
|
|
d3.select('#chart-container').selectAll('*').remove();
|
|
|
|
const svg = d3.select('#chart-container')
|
|
.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height)
|
|
.style('font-family', 'Inter, sans-serif');
|
|
|
|
// Create hierarchy
|
|
const root = d3.hierarchy(data)
|
|
.sum(d => {
|
|
if (d.type === 'node') return 0;
|
|
return d[currentMetric] || 0;
|
|
})
|
|
.sort((a, b) => b.value - a.value);
|
|
|
|
// Create Treemap
|
|
d3.treemap()
|
|
.size([width, height])
|
|
.paddingTop(28)
|
|
.paddingRight(8)
|
|
.paddingInner(4)
|
|
.paddingBottom(8)
|
|
.paddingLeft(8)
|
|
.round(true)(root);
|
|
|
|
// Get nodes and pods
|
|
const nodes = root.descendants().filter(d => d.depth === 1); // Nodes
|
|
const pods = root.leaves(); // Pods and Unused
|
|
|
|
// Color scale for pods
|
|
const getColor = (d) => {
|
|
if (d.data.type === 'unused') return 'var(--color-unused)';
|
|
if (d.data.status === 'Running') return 'var(--color-running)';
|
|
if (d.data.status === 'Pending') return 'var(--color-pending)';
|
|
return 'var(--color-failed)';
|
|
};
|
|
|
|
// Draw Node Groups
|
|
const nodeGroups = svg.selectAll('.node-group')
|
|
.data(nodes)
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', 'node-group')
|
|
.attr('transform', d => `translate(${d.x0},${d.y0})`);
|
|
|
|
// Node Background
|
|
nodeGroups.append('rect')
|
|
.attr('class', 'node-rect')
|
|
.attr('width', d => Math.max(0, d.x1 - d.x0))
|
|
.attr('height', d => Math.max(0, d.y1 - d.y0));
|
|
|
|
// Node Label
|
|
nodeGroups.append('text')
|
|
.attr('class', 'node-label')
|
|
.attr('x', 8)
|
|
.attr('y', 20)
|
|
.text(d => d.data.name);
|
|
|
|
// Draw Pods
|
|
const podGroups = svg.selectAll('.pod-group')
|
|
.data(pods)
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', 'pod-group')
|
|
.attr('transform', d => `translate(${d.x0},${d.y0})`);
|
|
|
|
// Pod Rect
|
|
podGroups.append('rect')
|
|
.attr('class', 'pod-rect')
|
|
.attr('width', d => Math.max(0, d.x1 - d.x0))
|
|
.attr('height', d => Math.max(0, d.y1 - d.y0))
|
|
.attr('fill', d => getColor(d))
|
|
.on('mousemove', function(event, d) {
|
|
const tooltip = document.getElementById('tooltip');
|
|
tooltip.classList.remove('hidden');
|
|
|
|
// Boundary detection for tooltip
|
|
const tWidth = 240;
|
|
let left = event.pageX + 15;
|
|
let top = event.pageY + 15;
|
|
|
|
if (left + tWidth > window.innerWidth) {
|
|
left = event.pageX - tWidth - 15;
|
|
}
|
|
|
|
tooltip.style.left = left + 'px';
|
|
tooltip.style.top = top + 'px';
|
|
|
|
let val = currentMetric === 'cpu' ? formatCpu(d.data[`${currentMetric}_raw`]) : formatBytes(d.data[`${currentMetric}_raw`]);
|
|
|
|
if (d.data.type === 'unused') {
|
|
tooltip.innerHTML = `
|
|
<div class="tooltip-title">Available Capacity</div>
|
|
<div class="tooltip-row">
|
|
<span class="tooltip-label">${currentMetric.toUpperCase()}:</span>
|
|
<span class="tooltip-value">${val}</span>
|
|
</div>
|
|
`;
|
|
} else {
|
|
tooltip.innerHTML = `
|
|
<div class="tooltip-title">${d.data.name}</div>
|
|
<div class="tooltip-row">
|
|
<span class="tooltip-label">Namespace:</span>
|
|
<span class="tooltip-value">${d.data.namespace}</span>
|
|
</div>
|
|
<div class="tooltip-row">
|
|
<span class="tooltip-label">Status:</span>
|
|
<span class="tooltip-value">${d.data.status}</span>
|
|
</div>
|
|
<div class="tooltip-row">
|
|
<span class="tooltip-label">${currentMetric.toUpperCase()}:</span>
|
|
<span class="tooltip-value">${val}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
})
|
|
.on('mouseout', function() {
|
|
document.getElementById('tooltip').classList.add('hidden');
|
|
});
|
|
|
|
// Pod Label (only if there is space)
|
|
podGroups.append('text')
|
|
.attr('class', 'pod-label')
|
|
.attr('x', 4)
|
|
.attr('y', 14)
|
|
.text(d => {
|
|
if (d.data.type === 'unused') return '';
|
|
const width = d.x1 - d.x0;
|
|
const height = d.y1 - d.y0;
|
|
if (width > 60 && height > 20) {
|
|
let txt = d.data.name;
|
|
if (txt.length * 5 > width) {
|
|
return txt.substring(0, Math.floor(width/5) - 2) + '..';
|
|
}
|
|
return txt;
|
|
}
|
|
return '';
|
|
});
|
|
}
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', () => {
|
|
if (clusterData) {
|
|
renderChart(clusterData);
|
|
}
|
|
});
|
|
|
|
// Initial fetch
|
|
fetchData();
|
|
// Poll every 30 seconds
|
|
setInterval(fetchData, 30000);
|