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
This commit is contained in:
@@ -1267,6 +1267,136 @@ async def get_qos_classification(
|
|||||||
logger.error(f"Error getting QoS classification: {e}")
|
logger.error(f"Error getting QoS classification: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(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")
|
@api_router.get("/resource-quotas")
|
||||||
async def get_resource_quotas(
|
async def get_resource_quotas(
|
||||||
namespace: Optional[str] = None,
|
namespace: Optional[str] = None,
|
||||||
|
|||||||
@@ -2081,34 +2081,58 @@
|
|||||||
// 2. Namespace Resource Distribution
|
// 2. Namespace Resource Distribution
|
||||||
async function loadNamespaceDistribution() {
|
async function loadNamespaceDistribution() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/cluster/status');
|
const response = await fetch('/api/v1/namespace-distribution');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Generate sample data for namespace distribution
|
// Convert real data to chart format
|
||||||
const distributionData = [
|
const distributionData = data.distribution.map(ns => ({
|
||||||
{ x: 'resource-governance', y: 25 },
|
x: ns.namespace,
|
||||||
{ x: 'redhat-ods-operator', y: 20 },
|
y: ns.cpu_requests,
|
||||||
{ x: 'node-gather', y: 15 },
|
podCount: ns.pod_count,
|
||||||
{ x: 'shishika01', y: 10 },
|
memoryRequests: ns.memory_requests
|
||||||
{ x: 'builds-test', y: 5 },
|
}));
|
||||||
{ x: 'Others', y: 25 }
|
|
||||||
];
|
|
||||||
|
|
||||||
createNamespaceDistributionChart(distributionData);
|
createNamespaceDistributionChart(distributionData, data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading namespace distribution:', 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');
|
const container = document.getElementById('namespace-distribution-chart');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// If no data, show empty state
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--pf-global--Color--300);">
|
||||||
|
<i class="fas fa-chart-pie" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
|
||||||
|
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Data Available</h3>
|
||||||
|
<p style="margin: 0;">No namespace resource data found</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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, {
|
const chart = React.createElement(Victory.VictoryPie, {
|
||||||
width: container.offsetWidth || 500,
|
width: container.offsetWidth || 500,
|
||||||
height: 300,
|
height: 300,
|
||||||
data: data,
|
data: chartData,
|
||||||
colorScale: ['#0066CC', '#CC0000', '#00CC66', '#FF8800', '#CC00CC', '#666666'],
|
colorScale: chartData.map(item => item.color),
|
||||||
padding: { top: 20, bottom: 20, left: 20, right: 20 },
|
padding: { top: 20, bottom: 20, left: 20, right: 20 },
|
||||||
style: {
|
style: {
|
||||||
parent: {
|
parent: {
|
||||||
@@ -2118,16 +2142,93 @@
|
|||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
fill: '#ccc',
|
fill: '#ccc',
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontFamily: 'Red Hat Text, sans-serif'
|
fontFamily: 'Red Hat Text, sans-serif'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
labelComponent: React.createElement(Victory.VictoryLabel, {
|
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 = `
|
||||||
|
<div style="margin-top: 16px; padding: 12px; background-color: #2B2B2B; border-radius: 4px; border: 1px solid #404040;">
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; text-align: center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300); margin-bottom: 4px;">Total CPU Requests</div>
|
||||||
|
<div style="font-size: 16px; font-weight: bold; color: var(--pf-global--Color--100);">${totalCpu.toFixed(2)} cores</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300); margin-bottom: 4px;">Total Pods</div>
|
||||||
|
<div style="font-size: 16px; font-weight: bold; color: var(--pf-global--Color--100);">${totalPods}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300); margin-bottom: 4px;">Namespaces</div>
|
||||||
|
<div style="font-size: 16px; font-weight: bold; color: var(--pf-global--Color--100);">${metadata.total_namespaces || data.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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
|
// 3. Issues by Severity Timeline
|
||||||
|
|||||||
Reference in New Issue
Block a user