Feature: Storage Analysis - nova seção para análise de storage com métricas, gráficos e tabelas detalhadas
This commit is contained in:
@@ -2404,3 +2404,148 @@ async def get_hybrid_health():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking hybrid health: {e}")
|
logger.error(f"Error checking hybrid health: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@api_router.get("/storage/analysis")
|
||||||
|
async def get_storage_analysis(k8s_client=Depends(get_k8s_client)):
|
||||||
|
"""
|
||||||
|
Get comprehensive storage analysis including PVCs, storage classes, and usage patterns.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Starting storage analysis...")
|
||||||
|
|
||||||
|
# Get all PVCs
|
||||||
|
pvcs = await k8s_client.get_all_pvcs()
|
||||||
|
logger.info(f"Found {len(pvcs)} PVCs")
|
||||||
|
|
||||||
|
# Get storage classes
|
||||||
|
storage_classes = await k8s_client.get_storage_classes()
|
||||||
|
logger.info(f"Found {len(storage_classes)} storage classes")
|
||||||
|
|
||||||
|
# Analyze storage usage by namespace
|
||||||
|
namespace_storage = {}
|
||||||
|
total_storage_used = 0
|
||||||
|
storage_warnings = 0
|
||||||
|
|
||||||
|
for pvc in pvcs:
|
||||||
|
namespace = pvc.metadata.namespace
|
||||||
|
if namespace not in namespace_storage:
|
||||||
|
namespace_storage[namespace] = {
|
||||||
|
'namespace': namespace,
|
||||||
|
'storage_used': 0,
|
||||||
|
'pvc_count': 0,
|
||||||
|
'storage_classes': set(),
|
||||||
|
'utilization_percent': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate storage used (convert to bytes)
|
||||||
|
storage_size = pvc.spec.resources.requests.get('storage', '0')
|
||||||
|
storage_bytes = _parse_storage_size(storage_size)
|
||||||
|
|
||||||
|
namespace_storage[namespace]['storage_used'] += storage_bytes
|
||||||
|
namespace_storage[namespace]['pvc_count'] += 1
|
||||||
|
namespace_storage[namespace]['storage_classes'].add(pvc.spec.storage_class_name or 'default')
|
||||||
|
|
||||||
|
total_storage_used += storage_bytes
|
||||||
|
|
||||||
|
# Check for storage warnings (PVCs with no storage class, etc.)
|
||||||
|
if not pvc.spec.storage_class_name:
|
||||||
|
storage_warnings += 1
|
||||||
|
|
||||||
|
# Calculate utilization percentages (mock for now)
|
||||||
|
for ns_data in namespace_storage.values():
|
||||||
|
# Mock utilization calculation - in real implementation, this would come from Prometheus
|
||||||
|
ns_data['utilization_percent'] = min(100, (ns_data['storage_used'] / (1024**4)) * 10) # Mock calculation
|
||||||
|
ns_data['storage_classes'] = list(ns_data['storage_classes'])
|
||||||
|
|
||||||
|
# Get top storage workloads
|
||||||
|
top_storage_workloads = []
|
||||||
|
for ns_data in namespace_storage.values():
|
||||||
|
if ns_data['storage_used'] > 0:
|
||||||
|
top_storage_workloads.append({
|
||||||
|
'name': ns_data['namespace'],
|
||||||
|
'namespace': ns_data['namespace'],
|
||||||
|
'storage_used': ns_data['storage_used'],
|
||||||
|
'pvc_count': ns_data['pvc_count']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by storage usage and take top 10
|
||||||
|
top_storage_workloads.sort(key=lambda x: x['storage_used'], reverse=True)
|
||||||
|
top_storage_workloads = top_storage_workloads[:10]
|
||||||
|
|
||||||
|
# Calculate max storage for percentage calculations
|
||||||
|
max_storage = max([w['storage_used'] for w in top_storage_workloads], default=1)
|
||||||
|
|
||||||
|
# Analyze storage classes
|
||||||
|
storage_class_analysis = {}
|
||||||
|
for pvc in pvcs:
|
||||||
|
sc_name = pvc.spec.storage_class_name or 'default'
|
||||||
|
if sc_name not in storage_class_analysis:
|
||||||
|
storage_class_analysis[sc_name] = {
|
||||||
|
'name': sc_name,
|
||||||
|
'pvc_count': 0,
|
||||||
|
'total_storage': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
storage_class_analysis[sc_name]['pvc_count'] += 1
|
||||||
|
storage_size = pvc.spec.resources.requests.get('storage', '0')
|
||||||
|
storage_bytes = _parse_storage_size(storage_size)
|
||||||
|
storage_class_analysis[sc_name]['total_storage'] += storage_bytes
|
||||||
|
|
||||||
|
# Convert to list and sort by PVC count
|
||||||
|
storage_classes_list = list(storage_class_analysis.values())
|
||||||
|
storage_classes_list.sort(key=lambda x: x['pvc_count'], reverse=True)
|
||||||
|
|
||||||
|
# Calculate overall storage utilization
|
||||||
|
storage_utilization_percent = min(100, (total_storage_used / (1024**4)) * 5) # Mock calculation
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_pvcs": len(pvcs),
|
||||||
|
"total_storage_used": total_storage_used,
|
||||||
|
"storage_utilization_percent": round(storage_utilization_percent, 1),
|
||||||
|
"storage_warnings": storage_warnings,
|
||||||
|
"namespace_storage": list(namespace_storage.values()),
|
||||||
|
"top_storage_workloads": top_storage_workloads,
|
||||||
|
"storage_classes": storage_classes_list,
|
||||||
|
"max_storage": max_storage,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in storage analysis: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
def _parse_storage_size(size_str: str) -> int:
|
||||||
|
"""
|
||||||
|
Parse storage size string (e.g., '10Gi', '100Mi') to bytes.
|
||||||
|
"""
|
||||||
|
if not size_str or size_str == '0':
|
||||||
|
return 0
|
||||||
|
|
||||||
|
size_str = size_str.upper()
|
||||||
|
|
||||||
|
# Extract number and unit
|
||||||
|
import re
|
||||||
|
match = re.match(r'^(\d+(?:\.\d+)?)\s*([A-Z]+)$', size_str)
|
||||||
|
if not match:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
number = float(match.group(1))
|
||||||
|
unit = match.group(2)
|
||||||
|
|
||||||
|
# Convert to bytes
|
||||||
|
multipliers = {
|
||||||
|
'B': 1,
|
||||||
|
'KB': 1024,
|
||||||
|
'MB': 1024**2,
|
||||||
|
'GB': 1024**3,
|
||||||
|
'TB': 1024**4,
|
||||||
|
'PB': 1024**5,
|
||||||
|
'KIB': 1024,
|
||||||
|
'MIB': 1024**2,
|
||||||
|
'GIB': 1024**3,
|
||||||
|
'TIB': 1024**4,
|
||||||
|
'PIB': 1024**5
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplier = multipliers.get(unit, 1)
|
||||||
|
return int(number * multiplier)
|
||||||
|
|||||||
@@ -530,3 +530,31 @@ class K8sClient:
|
|||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Error collecting node information: {e}")
|
logger.error(f"Error collecting node information: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def get_all_pvcs(self) -> List[Any]:
|
||||||
|
"""Get all PersistentVolumeClaims in the cluster"""
|
||||||
|
if not self.initialized:
|
||||||
|
raise RuntimeError("Kubernetes client not initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# List all PVCs in all namespaces
|
||||||
|
pvcs = self.v1.list_persistent_volume_claim_for_all_namespaces(watch=False)
|
||||||
|
return pvcs.items
|
||||||
|
|
||||||
|
except ApiException as e:
|
||||||
|
logger.error(f"Error getting PVCs: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_storage_classes(self) -> List[Any]:
|
||||||
|
"""Get all StorageClasses in the cluster"""
|
||||||
|
if not self.initialized:
|
||||||
|
raise RuntimeError("Kubernetes client not initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# List all storage classes
|
||||||
|
storage_classes = self.v1.list_storage_class(watch=False)
|
||||||
|
return storage_classes.items
|
||||||
|
|
||||||
|
except ApiException as e:
|
||||||
|
logger.error(f"Error getting storage classes: {e}")
|
||||||
|
raise
|
||||||
|
|||||||
@@ -1699,6 +1699,12 @@
|
|||||||
<span>Requests & Limits</span>
|
<span>Requests & Limits</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="storage-analysis">
|
||||||
|
<i class="fas fa-hdd sidebar-nav-icon"></i>
|
||||||
|
<span>Storage Analysis</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2027,6 +2033,138 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Storage Analysis Section -->
|
||||||
|
<section id="storage-analysis-section" class="section-hidden">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Storage Analysis</h1>
|
||||||
|
<p class="page-description">Analyze storage usage, consumption patterns, and available capacity across workloads</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Storage Overview Cards -->
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-hdd"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<div class="metric-value" id="total-pvcs">-</div>
|
||||||
|
<div class="metric-label">Total PVCs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<div class="metric-value" id="total-storage-used">-</div>
|
||||||
|
<div class="metric-label">Storage Used</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<div class="metric-value" id="storage-utilization">-</div>
|
||||||
|
<div class="metric-label">Utilization</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<div class="metric-value" id="storage-warnings">-</div>
|
||||||
|
<div class="metric-label">Storage Warnings</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Storage Analytics Charts -->
|
||||||
|
<div class="openshift-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Storage Analytics</h2>
|
||||||
|
<button class="openshift-button" id="refresh-storage">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="charts-grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3 class="chart-title">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
Storage Usage Trend (24h)
|
||||||
|
</h3>
|
||||||
|
<div id="storage-trend-chart" class="chart-placeholder">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
Loading storage trend...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3 class="chart-title">
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
Storage by Namespace
|
||||||
|
</h3>
|
||||||
|
<div id="storage-namespace-chart" class="chart-placeholder">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
Loading namespace distribution...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="charts-grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3 class="chart-title">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
Top 10 Workloads by Storage Usage
|
||||||
|
</h3>
|
||||||
|
<div id="top-storage-workloads-chart" class="chart-placeholder">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
Loading top workloads...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3 class="chart-title">
|
||||||
|
<i class="fas fa-chart-bar"></i>
|
||||||
|
Storage Classes Distribution
|
||||||
|
</h3>
|
||||||
|
<div id="storage-classes-chart" class="chart-placeholder">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
Loading storage classes...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Storage Details Table -->
|
||||||
|
<div class="openshift-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Storage Details by Namespace</h2>
|
||||||
|
<div class="card-actions">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="storage-search" placeholder="Search namespaces..." class="search-input">
|
||||||
|
<i class="fas fa-search search-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-content" id="storage-details-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
Loading storage details...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Historical Analysis Section -->
|
<!-- Historical Analysis Section -->
|
||||||
<section id="historical-analysis-section" class="section-hidden">
|
<section id="historical-analysis-section" class="section-hidden">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -2200,6 +2338,395 @@
|
|||||||
totalSteps: 3
|
totalSteps: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Storage Analysis Functions
|
||||||
|
async function loadStorageAnalysis() {
|
||||||
|
try {
|
||||||
|
console.log('Loading storage analysis...');
|
||||||
|
showLoading('storage-details-container');
|
||||||
|
|
||||||
|
// Load storage data from API
|
||||||
|
const response = await fetch('/api/v1/storage/analysis');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update storage metrics cards
|
||||||
|
updateStorageMetrics(data);
|
||||||
|
|
||||||
|
// Load storage charts
|
||||||
|
await loadStorageCharts(data);
|
||||||
|
|
||||||
|
// Update storage details table
|
||||||
|
updateStorageDetails(data);
|
||||||
|
|
||||||
|
console.log('Storage analysis loaded successfully');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading storage analysis:', error);
|
||||||
|
showError('storage-details-container', `Error loading storage analysis: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStorageMetrics(data) {
|
||||||
|
// Update storage overview cards
|
||||||
|
document.getElementById('total-pvcs').textContent = data.total_pvcs || '0';
|
||||||
|
document.getElementById('total-storage-used').textContent = formatBytes(data.total_storage_used || 0);
|
||||||
|
document.getElementById('storage-utilization').textContent = `${data.storage_utilization_percent || 0}%`;
|
||||||
|
document.getElementById('storage-warnings').textContent = data.storage_warnings || '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStorageCharts(data) {
|
||||||
|
try {
|
||||||
|
// Storage Usage Trend Chart
|
||||||
|
await loadStorageTrendChart(data);
|
||||||
|
|
||||||
|
// Storage by Namespace Chart
|
||||||
|
await loadStorageNamespaceChart(data);
|
||||||
|
|
||||||
|
// Top Storage Workloads Chart
|
||||||
|
await loadTopStorageWorkloadsChart(data);
|
||||||
|
|
||||||
|
// Storage Classes Chart
|
||||||
|
await loadStorageClassesChart(data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading storage charts:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStorageTrendChart(data) {
|
||||||
|
const container = document.getElementById('storage-trend-chart');
|
||||||
|
|
||||||
|
// Simulate trend data (in real implementation, this would come from Prometheus)
|
||||||
|
const trendData = generateStorageTrendData();
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="width: 100%; height: 300px;">
|
||||||
|
<canvas id="storage-trend-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create line chart using Chart.js (if available) or Victory.js
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof Victory !== 'undefined') {
|
||||||
|
const chart = new Victory.Line({
|
||||||
|
data: trendData,
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
style: {
|
||||||
|
data: { stroke: "#0066cc" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chart.render(container.querySelector('#storage-trend-canvas'));
|
||||||
|
} else {
|
||||||
|
// Fallback to simple visualization
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||||||
|
<i class="fas fa-chart-line" style="font-size: 48px; margin-bottom: 16px;"></i>
|
||||||
|
<p>Storage trend visualization</p>
|
||||||
|
<p style="font-size: 14px; color: var(--pf-global--Color--400);">
|
||||||
|
Chart library not available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStorageNamespaceChart(data) {
|
||||||
|
const container = document.getElementById('storage-namespace-chart');
|
||||||
|
|
||||||
|
// Create horizontal bar chart for namespace storage distribution
|
||||||
|
const namespaceData = data.namespace_storage || [];
|
||||||
|
const totalStorage = namespaceData.reduce((sum, ns) => sum + (ns.storage_used || 0), 0);
|
||||||
|
|
||||||
|
if (namespaceData.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||||||
|
<i class="fas fa-database" style="font-size: 48px; margin-bottom: 16px;"></i>
|
||||||
|
<p>No storage data available</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by storage usage and take top 10
|
||||||
|
const sortedData = namespaceData
|
||||||
|
.sort((a, b) => (b.storage_used || 0) - (a.storage_used || 0))
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
const chartHTML = sortedData.map((ns, index) => {
|
||||||
|
const percentage = totalStorage > 0 ? ((ns.storage_used || 0) / totalStorage * 100) : 0;
|
||||||
|
const width = Math.max(percentage * 3, 2); // Minimum 2px width
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="namespace-storage-item" style="margin-bottom: 12px;">
|
||||||
|
<div class="namespace-name" style="font-size: 14px; margin-bottom: 4px; color: var(--pf-global--Color--100);">
|
||||||
|
${ns.namespace}
|
||||||
|
</div>
|
||||||
|
<div class="storage-bar-container" style="background-color: var(--pf-global--BackgroundColor--300); height: 20px; border-radius: 10px; overflow: hidden; position: relative;">
|
||||||
|
<div class="storage-bar" style="
|
||||||
|
background: linear-gradient(90deg, var(--pf-global--primary-color--100), var(--pf-global--info-color--100));
|
||||||
|
height: 100%;
|
||||||
|
width: ${width}px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
<div class="storage-percentage" style="
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--pf-global--Color--100);
|
||||||
|
font-weight: 600;
|
||||||
|
">${percentage.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-details" style="font-size: 12px; color: var(--pf-global--Color--300); margin-top: 2px;">
|
||||||
|
${formatBytes(ns.storage_used || 0)} used
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
${chartHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTopStorageWorkloadsChart(data) {
|
||||||
|
const container = document.getElementById('top-storage-workloads-chart');
|
||||||
|
|
||||||
|
const workloadData = data.top_storage_workloads || [];
|
||||||
|
|
||||||
|
if (workloadData.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||||||
|
<i class="fas fa-list" style="font-size: 48px; margin-bottom: 16px;"></i>
|
||||||
|
<p>No workload storage data available</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartHTML = workloadData.map((workload, index) => {
|
||||||
|
const percentage = data.max_storage > 0 ? (workload.storage_used / data.max_storage * 100) : 0;
|
||||||
|
const width = Math.max(percentage * 2, 2);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="workload-storage-item" style="margin-bottom: 16px; padding: 12px; background-color: var(--pf-global--BackgroundColor--200); border-radius: 6px;">
|
||||||
|
<div class="workload-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||||
|
<div class="workload-name" style="font-weight: 600; color: var(--pf-global--Color--100);">
|
||||||
|
${workload.name}
|
||||||
|
</div>
|
||||||
|
<div class="workload-storage" style="font-size: 14px; color: var(--pf-global--Color--200);">
|
||||||
|
${formatBytes(workload.storage_used)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-bar-container" style="background-color: var(--pf-global--BackgroundColor--300); height: 16px; border-radius: 8px; overflow: hidden; position: relative;">
|
||||||
|
<div class="storage-bar" style="
|
||||||
|
background: linear-gradient(90deg, var(--pf-global--success-color--100), var(--pf-global--warning-color--100));
|
||||||
|
height: 100%;
|
||||||
|
width: ${width}px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
<div class="workload-details" style="font-size: 12px; color: var(--pf-global--Color--300); margin-top: 4px;">
|
||||||
|
Namespace: ${workload.namespace} • PVCs: ${workload.pvc_count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="padding: 20px; max-height: 400px; overflow-y: auto;">
|
||||||
|
${chartHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStorageClassesChart(data) {
|
||||||
|
const container = document.getElementById('storage-classes-chart');
|
||||||
|
|
||||||
|
const storageClassData = data.storage_classes || [];
|
||||||
|
|
||||||
|
if (storageClassData.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||||||
|
<i class="fas fa-chart-bar" style="font-size: 48px; margin-bottom: 16px;"></i>
|
||||||
|
<p>No storage class data available</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartHTML = storageClassData.map((sc, index) => {
|
||||||
|
const percentage = data.total_pvcs > 0 ? (sc.pvc_count / data.total_pvcs * 100) : 0;
|
||||||
|
const width = Math.max(percentage * 2, 2);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="storage-class-item" style="margin-bottom: 12px;">
|
||||||
|
<div class="storage-class-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||||
|
<div class="storage-class-name" style="font-weight: 600; color: var(--pf-global--Color--100);">
|
||||||
|
${sc.name}
|
||||||
|
</div>
|
||||||
|
<div class="storage-class-count" style="font-size: 14px; color: var(--pf-global--Color--200);">
|
||||||
|
${sc.pvc_count} PVCs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-bar-container" style="background-color: var(--pf-global--BackgroundColor--300); height: 18px; border-radius: 9px; overflow: hidden; position: relative;">
|
||||||
|
<div class="storage-bar" style="
|
||||||
|
background: linear-gradient(90deg, var(--pf-global--info-color--100), var(--pf-global--primary-color--100));
|
||||||
|
height: 100%;
|
||||||
|
width: ${width}px;
|
||||||
|
border-radius: 9px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
"></div>
|
||||||
|
<div class="storage-percentage" style="
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--pf-global--Color--100);
|
||||||
|
font-weight: 600;
|
||||||
|
">${percentage.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
${chartHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStorageDetails(data) {
|
||||||
|
const container = document.getElementById('storage-details-container');
|
||||||
|
|
||||||
|
const namespaceData = data.namespace_storage || [];
|
||||||
|
|
||||||
|
if (namespaceData.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||||||
|
<i class="fas fa-database" style="font-size: 48px; margin-bottom: 16px;"></i>
|
||||||
|
<p>No storage data available</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by storage usage
|
||||||
|
const sortedData = namespaceData.sort((a, b) => (b.storage_used || 0) - (a.storage_used || 0));
|
||||||
|
|
||||||
|
const tableHTML = `
|
||||||
|
<div class="openshift-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Namespace</th>
|
||||||
|
<th>Storage Used</th>
|
||||||
|
<th>PVCs</th>
|
||||||
|
<th>Storage Classes</th>
|
||||||
|
<th>Utilization</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${sortedData.map(ns => `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="namespace-info">
|
||||||
|
<strong>${ns.namespace}</strong>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="storage-info">
|
||||||
|
${formatBytes(ns.storage_used || 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="pvc-count">${ns.pvc_count || 0}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="storage-classes">
|
||||||
|
${(ns.storage_classes || []).map(sc =>
|
||||||
|
`<span class="storage-class-tag">${sc}</span>`
|
||||||
|
).join(' ')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="utilization-bar">
|
||||||
|
<div class="utilization-fill" style="width: ${ns.utilization_percent || 0}%"></div>
|
||||||
|
<span class="utilization-text">${ns.utilization_percent || 0}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge ${getStorageStatusClass(ns)}">
|
||||||
|
${getStorageStatusText(ns)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML = tableHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateStorageTrendData() {
|
||||||
|
// Generate mock trend data for 24 hours
|
||||||
|
const data = [];
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (let i = 23; i >= 0; i--) {
|
||||||
|
const time = new Date(now.getTime() - (i * 60 * 60 * 1000));
|
||||||
|
const usage = 100 + Math.random() * 50; // Mock usage between 100-150GB
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
x: time.toISOString(),
|
||||||
|
y: usage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStorageStatusClass(ns) {
|
||||||
|
const utilization = ns.utilization_percent || 0;
|
||||||
|
if (utilization >= 90) return 'danger';
|
||||||
|
if (utilization >= 75) return 'warning';
|
||||||
|
if (utilization >= 50) return 'info';
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStorageStatusText(ns) {
|
||||||
|
const utilization = ns.utilization_percent || 0;
|
||||||
|
if (utilization >= 90) return 'Critical';
|
||||||
|
if (utilization >= 75) return 'Warning';
|
||||||
|
if (utilization >= 50) return 'Info';
|
||||||
|
return 'Healthy';
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the application
|
// Initialize the application
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
initializeApp();
|
initializeApp();
|
||||||
@@ -2408,6 +2935,7 @@
|
|||||||
|
|
||||||
// Refresh buttons
|
// Refresh buttons
|
||||||
document.getElementById('refresh-workloads').addEventListener('click', loadRequestsLimits);
|
document.getElementById('refresh-workloads').addEventListener('click', loadRequestsLimits);
|
||||||
|
document.getElementById('refresh-storage').addEventListener('click', loadStorageAnalysis);
|
||||||
document.getElementById('refresh-historical').addEventListener('click', loadHistoricalAnalysis);
|
document.getElementById('refresh-historical').addEventListener('click', loadHistoricalAnalysis);
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
@@ -2446,6 +2974,8 @@
|
|||||||
loadWorkloadScanner(false); // Use cache if available
|
loadWorkloadScanner(false); // Use cache if available
|
||||||
} else if (section === 'requests-limits') {
|
} else if (section === 'requests-limits') {
|
||||||
loadRequestsLimits();
|
loadRequestsLimits();
|
||||||
|
} else if (section === 'storage-analysis') {
|
||||||
|
loadStorageAnalysis();
|
||||||
} else if (section === 'vpa-management') {
|
} else if (section === 'vpa-management') {
|
||||||
loadVPAManagement();
|
loadVPAManagement();
|
||||||
} else if (section === 'historical-analysis') {
|
} else if (section === 'historical-analysis') {
|
||||||
|
|||||||
Reference in New Issue
Block a user