k8s-pulse/static/app.js

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);