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:
|
||||
logger.error(f"Error checking hybrid health: {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:
|
||||
logger.error(f"Error collecting node information: {e}")
|
||||
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>
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -2027,6 +2033,138 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<section id="historical-analysis-section" class="section-hidden">
|
||||
<div class="page-header">
|
||||
@@ -2200,6 +2338,395 @@
|
||||
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
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeApp();
|
||||
@@ -2408,6 +2935,7 @@
|
||||
|
||||
// Refresh buttons
|
||||
document.getElementById('refresh-workloads').addEventListener('click', loadRequestsLimits);
|
||||
document.getElementById('refresh-storage').addEventListener('click', loadStorageAnalysis);
|
||||
document.getElementById('refresh-historical').addEventListener('click', loadHistoricalAnalysis);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
@@ -2446,6 +2974,8 @@
|
||||
loadWorkloadScanner(false); // Use cache if available
|
||||
} else if (section === 'requests-limits') {
|
||||
loadRequestsLimits();
|
||||
} else if (section === 'storage-analysis') {
|
||||
loadStorageAnalysis();
|
||||
} else if (section === 'vpa-management') {
|
||||
loadVPAManagement();
|
||||
} else if (section === 'historical-analysis') {
|
||||
|
||||
Reference in New Issue
Block a user