feat: implement Chart.js graphs for Historical Analysis
- Add Chart.js 4.4.0 and date adapter for time series graphs - Implement createCPUChart and createMemoryChart functions - Update updateWorkloadDetailsAccordion to show interactive graphs - Add getCurrentValue, getAverageValue, getPeakValue helper functions - Display CPU and Memory usage over 24h with real-time data - Show current, average, and peak values below graphs - Use working Prometheus queries from metrics endpoint
This commit is contained in:
@@ -1332,3 +1332,173 @@ class HistoricalAnalysisService:
|
|||||||
'error': str(e),
|
'error': str(e),
|
||||||
'recommendations': []
|
'recommendations': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def get_cpu_usage_history(self, namespace: str, workload: str, time_range: str = "24h") -> Dict[str, Any]:
|
||||||
|
"""Get CPU usage history for a workload using working Prometheus queries"""
|
||||||
|
try:
|
||||||
|
# Use the working query from the metrics endpoint
|
||||||
|
cpu_usage_query = f'rate(container_cpu_usage_seconds_total{{namespace="{namespace}", pod=~"{workload}.*"}}[5m])'
|
||||||
|
|
||||||
|
# Calculate time range
|
||||||
|
end_time = datetime.now()
|
||||||
|
start_time = end_time - timedelta(seconds=self.time_ranges.get(time_range, 86400))
|
||||||
|
|
||||||
|
# Query Prometheus
|
||||||
|
data = await self._query_prometheus(cpu_usage_query, start_time, end_time)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return {
|
||||||
|
"workload": workload,
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"data": [],
|
||||||
|
"message": "No CPU usage data available"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format data for Chart.js
|
||||||
|
chart_data = []
|
||||||
|
for point in data:
|
||||||
|
if len(point) >= 2 and point[1] != 'NaN':
|
||||||
|
timestamp = int(point[0] * 1000) # Convert to milliseconds
|
||||||
|
value = self._safe_float(point[1])
|
||||||
|
chart_data.append({
|
||||||
|
"x": timestamp,
|
||||||
|
"y": value
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"workload": workload,
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"data": chart_data,
|
||||||
|
"query": cpu_usage_query
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting CPU usage history: {str(e)}")
|
||||||
|
return {
|
||||||
|
"workload": workload,
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"data": [],
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_memory_usage_history(self, namespace: str, workload: str, time_range: str = "24h") -> Dict[str, Any]:
|
||||||
|
"""Get memory usage history for a workload using working Prometheus queries"""
|
||||||
|
try:
|
||||||
|
# Use the working query from the metrics endpoint
|
||||||
|
memory_usage_query = f'container_memory_working_set_bytes{{namespace="{namespace}", pod=~"{workload}.*", container!="", image!=""}}'
|
||||||
|
|
||||||
|
# Calculate time range
|
||||||
|
end_time = datetime.now()
|
||||||
|
start_time = end_time - timedelta(seconds=self.time_ranges.get(time_range, 86400))
|
||||||
|
|
||||||
|
# Query Prometheus
|
||||||
|
data = await self._query_prometheus(memory_usage_query, start_time, end_time)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return {
|
||||||
|
"workload": workload,
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"data": [],
|
||||||
|
"message": "No memory usage data available"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format data for Chart.js (convert bytes to MB)
|
||||||
|
chart_data = []
|
||||||
|
for point in data:
|
||||||
|
if len(point) >= 2 and point[1] != 'NaN':
|
||||||
|
timestamp = int(point[0] * 1000) # Convert to milliseconds
|
||||||
|
value = self._safe_float(point[1]) / (1024 * 1024) # Convert to MB
|
||||||
|
chart_data.append({
|
||||||
|
"x": timestamp,
|
||||||
|
"y": value
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"workload": workload,
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"data": chart_data,
|
||||||
|
"query": memory_usage_query
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting memory usage history: {str(e)}")
|
||||||
|
return {
|
||||||
|
"workload": workload,
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"data": [],
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def generate_recommendations(self, namespace: str, workload: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate recommendations based on historical data"""
|
||||||
|
try:
|
||||||
|
# Get current usage data
|
||||||
|
cpu_data = await self.get_cpu_usage_history(namespace, workload, "24h")
|
||||||
|
memory_data = await self.get_memory_usage_history(namespace, workload, "24h")
|
||||||
|
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
# Analyze CPU data
|
||||||
|
if cpu_data.get("data"):
|
||||||
|
cpu_values = [point["y"] for point in cpu_data["data"]]
|
||||||
|
if cpu_values:
|
||||||
|
avg_cpu = sum(cpu_values) / len(cpu_values)
|
||||||
|
max_cpu = max(cpu_values)
|
||||||
|
|
||||||
|
if avg_cpu < 0.1: # Less than 100m
|
||||||
|
recommendations.append({
|
||||||
|
"type": "cpu_optimization",
|
||||||
|
"severity": "info",
|
||||||
|
"message": f"CPU usage is very low (avg: {avg_cpu:.3f} cores). Consider reducing CPU requests.",
|
||||||
|
"current_usage": f"{avg_cpu:.3f} cores",
|
||||||
|
"recommendation": "Reduce CPU requests to match actual usage"
|
||||||
|
})
|
||||||
|
elif max_cpu > 0.8: # More than 800m
|
||||||
|
recommendations.append({
|
||||||
|
"type": "cpu_scaling",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": f"CPU usage peaks at {max_cpu:.3f} cores. Consider increasing CPU limits.",
|
||||||
|
"current_usage": f"{max_cpu:.3f} cores",
|
||||||
|
"recommendation": "Increase CPU limits to handle peak usage"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Analyze memory data
|
||||||
|
if memory_data.get("data"):
|
||||||
|
memory_values = [point["y"] for point in memory_data["data"]]
|
||||||
|
if memory_values:
|
||||||
|
avg_memory = sum(memory_values) / len(memory_values)
|
||||||
|
max_memory = max(memory_values)
|
||||||
|
|
||||||
|
if avg_memory < 100: # Less than 100MB
|
||||||
|
recommendations.append({
|
||||||
|
"type": "memory_optimization",
|
||||||
|
"severity": "info",
|
||||||
|
"message": f"Memory usage is very low (avg: {avg_memory:.1f} MB). Consider reducing memory requests.",
|
||||||
|
"current_usage": f"{avg_memory:.1f} MB",
|
||||||
|
"recommendation": "Reduce memory requests to match actual usage"
|
||||||
|
})
|
||||||
|
elif max_memory > 1000: # More than 1GB
|
||||||
|
recommendations.append({
|
||||||
|
"type": "memory_scaling",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": f"Memory usage peaks at {max_memory:.1f} MB. Consider increasing memory limits.",
|
||||||
|
"current_usage": f"{max_memory:.1f} MB",
|
||||||
|
"recommendation": "Increase memory limits to handle peak usage"
|
||||||
|
})
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating recommendations: {str(e)}")
|
||||||
|
return [{
|
||||||
|
"type": "error",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Error generating recommendations: {str(e)}",
|
||||||
|
"recommendation": "Check Prometheus connectivity and workload configuration"
|
||||||
|
}]
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
<!-- Font Awesome for icons -->
|
<!-- Font Awesome for icons -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Chart.js for historical analysis graphs -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||||
|
|
||||||
<!-- Custom OpenShift-like styles -->
|
<!-- Custom OpenShift-like styles -->
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -1185,6 +1189,10 @@
|
|||||||
function updateWorkloadDetailsAccordion(data, index) {
|
function updateWorkloadDetailsAccordion(data, index) {
|
||||||
const container = document.getElementById(`details-content-${index}`);
|
const container = document.getElementById(`details-content-${index}`);
|
||||||
|
|
||||||
|
// Create chart containers with unique IDs
|
||||||
|
const cpuChartId = `cpu-chart-${index}`;
|
||||||
|
const memoryChartId = `memory-chart-${index}`;
|
||||||
|
|
||||||
// Parse CPU and Memory data
|
// Parse CPU and Memory data
|
||||||
const cpuData = data.cpu_data || {};
|
const cpuData = data.cpu_data || {};
|
||||||
const memoryData = data.memory_data || {};
|
const memoryData = data.memory_data || {};
|
||||||
@@ -1192,46 +1200,32 @@
|
|||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div style="padding: 24px; background-color: #1E1E1E; border-radius: 8px; margin: 16px 0;">
|
<div style="padding: 24px; background-color: #1E1E1E; border-radius: 8px; margin: 16px 0;">
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px;">
|
||||||
<div class="openshift-card">
|
<div class="openshift-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">
|
<h3 class="card-title">
|
||||||
<i class="fas fa-microchip" style="margin-right: 8px; color: var(--pf-global--info-color--100);"></i>
|
<i class="fas fa-microchip" style="margin-right: 8px; color: var(--pf-global--info-color--100);"></i>
|
||||||
CPU Usage
|
CPU Usage (24h)
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
${cpuData.current ? `
|
<div style="height: 300px; position: relative;">
|
||||||
<div style="margin-bottom: 16px;">
|
<canvas id="${cpuChartId}" width="400" height="200"></canvas>
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
||||||
<span>Current Usage:</span>
|
|
||||||
<strong style="color: var(--pf-global--Color--100);">${cpuData.current} cores</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background-color: #404040; height: 8px; border-radius: 4px; overflow: hidden;">
|
${cpuData.data && cpuData.data.length > 0 ? `
|
||||||
<div style="background-color: var(--pf-global--info-color--100); height: 100%; width: ${Math.min((cpuData.current / (cpuData.limit || 1)) * 100, 100)}%;"></div>
|
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
|
||||||
|
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Current</div>
|
||||||
|
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getCurrentValue(cpuData.data)} cores</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Average</div>
|
||||||
|
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getAverageValue(cpuData.data)} cores</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
|
||||||
${cpuData.average ? `
|
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Peak</div>
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="font-weight: bold; color: var(--pf-global--warning-color--100);">${getPeakValue(cpuData.data)} cores</div>
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
||||||
<span>Average (24h):</span>
|
|
||||||
<strong style="color: var(--pf-global--Color--100);">${cpuData.average} cores</strong>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${cpuData.peak ? `
|
|
||||||
<div style="margin-bottom: 16px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
||||||
<span>Peak (24h):</span>
|
|
||||||
<strong style="color: var(--pf-global--warning-color--100);">${cpuData.peak} cores</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${!cpuData.current && !cpuData.average && !cpuData.peak ? `
|
|
||||||
<div style="text-align: center; color: var(--pf-global--Color--300);">
|
|
||||||
<i class="fas fa-chart-line" style="font-size: 32px; margin-bottom: 8px; color: var(--pf-global--Color--400);"></i>
|
|
||||||
<p>CPU usage data not available</p>
|
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -1241,41 +1235,27 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">
|
<h3 class="card-title">
|
||||||
<i class="fas fa-memory" style="margin-right: 8px; color: var(--pf-global--warning-color--100);"></i>
|
<i class="fas fa-memory" style="margin-right: 8px; color: var(--pf-global--warning-color--100);"></i>
|
||||||
Memory Usage
|
Memory Usage (24h)
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
${memoryData.current ? `
|
<div style="height: 300px; position: relative;">
|
||||||
<div style="margin-bottom: 16px;">
|
<canvas id="${memoryChartId}" width="400" height="200"></canvas>
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
||||||
<span>Current Usage:</span>
|
|
||||||
<strong style="color: var(--pf-global--Color--100);">${memoryData.current}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background-color: #404040; height: 8px; border-radius: 4px; overflow: hidden;">
|
${memoryData.data && memoryData.data.length > 0 ? `
|
||||||
<div style="background-color: var(--pf-global--warning-color--100); height: 100%; width: ${Math.min((memoryData.current_bytes / (memoryData.limit_bytes || 1)) * 100, 100)}%;"></div>
|
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
|
||||||
|
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Current</div>
|
||||||
|
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getCurrentValue(memoryData.data)} MB</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
|
||||||
|
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Average</div>
|
||||||
|
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getAverageValue(memoryData.data)} MB</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
|
||||||
${memoryData.average ? `
|
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Peak</div>
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="font-weight: bold; color: var(--pf-global--warning-color--100);">${getPeakValue(memoryData.data)} MB</div>
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
||||||
<span>Average (24h):</span>
|
|
||||||
<strong style="color: var(--pf-global--Color--100);">${memoryData.average}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${memoryData.peak ? `
|
|
||||||
<div style="margin-bottom: 16px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
||||||
<span>Peak (24h):</span>
|
|
||||||
<strong style="color: var(--pf-global--danger-color--100);">${memoryData.peak}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${!memoryData.current && !memoryData.average && !memoryData.peak ? `
|
|
||||||
<div style="text-align: center; color: var(--pf-global--Color--300);">
|
|
||||||
<i class="fas fa-chart-line" style="font-size: 32px; margin-bottom: 8px; color: var(--pf-global--Color--400);"></i>
|
|
||||||
<p>Memory usage data not available</p>
|
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -1291,14 +1271,165 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
<ul style="margin: 0; padding-left: 20px;">
|
${recommendations.map(rec => `
|
||||||
${recommendations.map(rec => `<li style="margin-bottom: 8px; color: var(--pf-global--Color--200);">${rec}</li>`).join('')}
|
<div style="margin-bottom: 16px; padding: 16px; background-color: #2B2B2B; border-radius: 6px; border-left: 4px solid ${getSeverityColor(rec.severity)};">
|
||||||
</ul>
|
<p style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);"><strong>${rec.type}:</strong> ${rec.message}</p>
|
||||||
|
<p style="margin: 0; color: var(--pf-global--Color--300);"><em>Recommendation:</em> ${rec.recommendation}</p>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Create charts after DOM is updated
|
||||||
|
setTimeout(() => {
|
||||||
|
createCPUChart(cpuChartId, cpuData);
|
||||||
|
createMemoryChart(memoryChartId, memoryData);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentValue(data) {
|
||||||
|
if (!data || data.length === 0) return '0.000';
|
||||||
|
const lastValue = data[data.length - 1];
|
||||||
|
return lastValue ? lastValue.y.toFixed(3) : '0.000';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAverageValue(data) {
|
||||||
|
if (!data || data.length === 0) return '0.000';
|
||||||
|
const sum = data.reduce((acc, point) => acc + point.y, 0);
|
||||||
|
return (sum / data.length).toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPeakValue(data) {
|
||||||
|
if (!data || data.length === 0) return '0.000';
|
||||||
|
const max = Math.max(...data.map(point => point.y));
|
||||||
|
return max.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCPUChart(canvasId, cpuData) {
|
||||||
|
const ctx = document.getElementById(canvasId);
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const chartData = cpuData?.data || [];
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
label: 'CPU Usage (cores)',
|
||||||
|
data: chartData,
|
||||||
|
borderColor: '#0066CC',
|
||||||
|
backgroundColor: 'rgba(0, 102, 204, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
displayFormats: {
|
||||||
|
hour: 'HH:mm',
|
||||||
|
day: 'MMM dd'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: '#404040'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
callback: function(value) {
|
||||||
|
return value.toFixed(3) + ' cores';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: '#404040'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMemoryChart(canvasId, memoryData) {
|
||||||
|
const ctx = document.getElementById(canvasId);
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const chartData = memoryData?.data || [];
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
label: 'Memory Usage (MB)',
|
||||||
|
data: chartData,
|
||||||
|
borderColor: '#CC0000',
|
||||||
|
backgroundColor: 'rgba(204, 0, 0, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
displayFormats: {
|
||||||
|
hour: 'HH:mm',
|
||||||
|
day: 'MMM dd'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: '#404040'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
callback: function(value) {
|
||||||
|
return value.toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: '#404040'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWorkloadDetails(data) {
|
function updateWorkloadDetails(data) {
|
||||||
|
|||||||
Reference in New Issue
Block a user