From eddc492d0eb573d378b63f26ca706e9a598adbd8 Mon Sep 17 00:00:00 2001 From: andersonid Date: Sat, 4 Oct 2025 11:43:22 -0300 Subject: [PATCH] Add real namespace distribution data for dashboard chart - Create new API endpoint /api/v1/namespace-distribution - Replace mock data with real cluster data - Add CPU and memory parsing functions - Update frontend to use real data with enhanced chart - Add hover effects and summary statistics --- app/api/routes.py | 130 +++++++++++++++++++++++++++++++++++++++ app/static/index.html | 137 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 249 insertions(+), 18 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index 243eea9..6a600a6 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1267,6 +1267,136 @@ async def get_qos_classification( logger.error(f"Error getting QoS classification: {e}") raise HTTPException(status_code=500, detail=str(e)) +@api_router.get("/namespace-distribution") +async def get_namespace_distribution( + k8s_client=Depends(get_k8s_client), + prometheus_client=Depends(get_prometheus_client) +): + """Get resource distribution by namespace for dashboard charts""" + try: + # Get all pods + pods = await k8s_client.get_all_pods() + + # Group pods by namespace and calculate resource usage + namespace_resources = {} + + for pod in pods: + namespace = pod.namespace + + if namespace not in namespace_resources: + namespace_resources[namespace] = { + 'namespace': namespace, + 'cpu_requests': 0.0, + 'memory_requests': 0.0, + 'cpu_limits': 0.0, + 'memory_limits': 0.0, + 'pod_count': 0 + } + + # Sum up resources from all containers in the pod + for container in pod.containers: + resources = container.get('resources', {}) + + # CPU requests and limits + cpu_req = resources.get('requests', {}).get('cpu', '0') + cpu_lim = resources.get('limits', {}).get('cpu', '0') + + # Memory requests and limits + mem_req = resources.get('requests', {}).get('memory', '0') + mem_lim = resources.get('limits', {}).get('memory', '0') + + # Convert to numeric values + namespace_resources[namespace]['cpu_requests'] += _parse_cpu_value(cpu_req) + namespace_resources[namespace]['cpu_limits'] += _parse_cpu_value(cpu_lim) + namespace_resources[namespace]['memory_requests'] += _parse_memory_value(mem_req) + namespace_resources[namespace]['memory_limits'] += _parse_memory_value(mem_lim) + + namespace_resources[namespace]['pod_count'] += 1 + + # Convert to list and sort by CPU requests (descending) + distribution_data = [] + for namespace, data in namespace_resources.items(): + distribution_data.append({ + 'namespace': namespace, + 'cpu_requests': data['cpu_requests'], + 'memory_requests': data['memory_requests'], + 'cpu_limits': data['cpu_limits'], + 'memory_limits': data['memory_limits'], + 'pod_count': data['pod_count'] + }) + + # Sort by CPU requests descending + distribution_data.sort(key=lambda x: x['cpu_requests'], reverse=True) + + # Take top 10 namespaces and group others + top_namespaces = distribution_data[:10] + others_data = distribution_data[10:] + + # Calculate "Others" total + others_total = { + 'namespace': 'Others', + 'cpu_requests': sum(ns['cpu_requests'] for ns in others_data), + 'memory_requests': sum(ns['memory_requests'] for ns in others_data), + 'cpu_limits': sum(ns['cpu_limits'] for ns in others_data), + 'memory_limits': sum(ns['memory_limits'] for ns in others_data), + 'pod_count': sum(ns['pod_count'] for ns in others_data) + } + + # Add "Others" if there are any + if others_total['cpu_requests'] > 0 or others_total['memory_requests'] > 0: + top_namespaces.append(others_total) + + return { + 'distribution': top_namespaces, + 'total_namespaces': len(distribution_data), + 'total_cpu_requests': sum(ns['cpu_requests'] for ns in distribution_data), + 'total_memory_requests': sum(ns['memory_requests'] for ns in distribution_data) + } + + except Exception as e: + logger.error(f"Error getting namespace distribution: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +def _parse_cpu_value(cpu_str: str) -> float: + """Parse CPU value from string (e.g., '100m' -> 0.1, '1' -> 1.0)""" + if not cpu_str or cpu_str == '0': + return 0.0 + + cpu_str = str(cpu_str).strip() + + if cpu_str.endswith('m'): + return float(cpu_str[:-1]) / 1000.0 + elif cpu_str.endswith('n'): + return float(cpu_str[:-1]) / 1000000000.0 + else: + return float(cpu_str) + +def _parse_memory_value(mem_str: str) -> float: + """Parse memory value from string (e.g., '128Mi' -> 134217728, '1Gi' -> 1073741824)""" + if not mem_str or mem_str == '0': + return 0.0 + + mem_str = str(mem_str).strip() + + if mem_str.endswith('Ki'): + return float(mem_str[:-2]) * 1024 + elif mem_str.endswith('Mi'): + return float(mem_str[:-2]) * 1024 * 1024 + elif mem_str.endswith('Gi'): + return float(mem_str[:-2]) * 1024 * 1024 * 1024 + elif mem_str.endswith('Ti'): + return float(mem_str[:-2]) * 1024 * 1024 * 1024 * 1024 + elif mem_str.endswith('K'): + return float(mem_str[:-1]) * 1000 + elif mem_str.endswith('M'): + return float(mem_str[:-1]) * 1000 * 1000 + elif mem_str.endswith('G'): + return float(mem_str[:-1]) * 1000 * 1000 * 1000 + elif mem_str.endswith('T'): + return float(mem_str[:-1]) * 1000 * 1000 * 1000 * 1000 + else: + return float(mem_str) + @api_router.get("/resource-quotas") async def get_resource_quotas( namespace: Optional[str] = None, diff --git a/app/static/index.html b/app/static/index.html index d4c6b68..99194fc 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -2081,34 +2081,58 @@ // 2. Namespace Resource Distribution async function loadNamespaceDistribution() { try { - const response = await fetch('/api/v1/cluster/status'); + const response = await fetch('/api/v1/namespace-distribution'); const data = await response.json(); - // Generate sample data for namespace distribution - const distributionData = [ - { x: 'resource-governance', y: 25 }, - { x: 'redhat-ods-operator', y: 20 }, - { x: 'node-gather', y: 15 }, - { x: 'shishika01', y: 10 }, - { x: 'builds-test', y: 5 }, - { x: 'Others', y: 25 } - ]; + // Convert real data to chart format + const distributionData = data.distribution.map(ns => ({ + x: ns.namespace, + y: ns.cpu_requests, + podCount: ns.pod_count, + memoryRequests: ns.memory_requests + })); - createNamespaceDistributionChart(distributionData); + createNamespaceDistributionChart(distributionData, data); } catch (error) { console.error('Error loading namespace distribution:', error); + // Fallback to empty chart + createNamespaceDistributionChart([], { total_namespaces: 0 }); } } - function createNamespaceDistributionChart(data) { + function createNamespaceDistributionChart(data, metadata = {}) { const container = document.getElementById('namespace-distribution-chart'); if (!container) return; + // If no data, show empty state + if (!data || data.length === 0) { + container.innerHTML = ` +
+ +

No Data Available

+

No namespace resource data found

+
+ `; + return; + } + + // Generate colors for namespaces + const colors = ['#0066CC', '#CC0000', '#00CC66', '#FF8800', '#CC00CC', '#666666', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']; + + // Prepare data for Victory chart + const chartData = data.map((item, index) => ({ + x: item.x, + y: item.y, + color: colors[index % colors.length], + podCount: item.podCount || 0, + memoryRequests: item.memoryRequests || 0 + })); + const chart = React.createElement(Victory.VictoryPie, { width: container.offsetWidth || 500, height: 300, - data: data, - colorScale: ['#0066CC', '#CC0000', '#00CC66', '#FF8800', '#CC00CC', '#666666'], + data: chartData, + colorScale: chartData.map(item => item.color), padding: { top: 20, bottom: 20, left: 20, right: 20 }, style: { parent: { @@ -2118,16 +2142,93 @@ }, labels: { fill: '#ccc', - fontSize: 12, + fontSize: 11, fontFamily: 'Red Hat Text, sans-serif' } }, labelComponent: React.createElement(Victory.VictoryLabel, { - style: { fill: '#ccc', fontSize: 12 } - }) + style: { + fill: '#ccc', + fontSize: 11, + fontFamily: 'Red Hat Text, sans-serif' + }, + labelPlacement: 'perpendicular', + text: (datum) => { + const cpuCores = datum.y.toFixed(2); + const podCount = datum.podCount; + return `${datum.x}\n${cpuCores} cores\n${podCount} pods`; + } + }), + events: [{ + target: "data", + eventHandlers: { + onMouseOver: () => { + return [{ + target: "labels", + mutation: (props) => { + return { + style: Object.assign({}, props.style, { fill: "#fff", fontSize: 12, fontWeight: "bold" }) + }; + } + }]; + }, + onMouseOut: () => { + return [{ + target: "labels", + mutation: (props) => { + return { + style: Object.assign({}, props.style, { fill: "#ccc", fontSize: 11, fontWeight: "normal" }) + }; + } + }]; + } + } + }] }); - ReactDOM.render(chart, container); + // Add summary information below the chart + const totalCpu = data.reduce((sum, item) => sum + item.y, 0); + const totalPods = data.reduce((sum, item) => sum + (item.podCount || 0), 0); + + const summaryHtml = ` +
+
+
+
Total CPU Requests
+
${totalCpu.toFixed(2)} cores
+
+
+
Total Pods
+
${totalPods}
+
+
+
Namespaces
+
${metadata.total_namespaces || data.length}
+
+
+
+ `; + + // Create a wrapper div to hold both chart and summary + const wrapper = document.createElement('div'); + wrapper.style.width = '100%'; + wrapper.style.height = '100%'; + wrapper.style.display = 'flex'; + wrapper.style.flexDirection = 'column'; + + const chartDiv = document.createElement('div'); + chartDiv.style.flex = '1'; + chartDiv.style.minHeight = '200px'; + + wrapper.appendChild(chartDiv); + wrapper.innerHTML += summaryHtml; + + // Clear container and add wrapper + container.innerHTML = ''; + container.appendChild(wrapper); + + // Render chart in the chart div + ReactDOM.render(chart, chartDiv); } // 3. Issues by Severity Timeline