Implement simplified UI/UX with health scores and grouped validations
This commit is contained in:
24
README.md
24
README.md
@@ -288,6 +288,30 @@ curl http://localhost:8080/health
|
||||
|
||||
---
|
||||
|
||||
### **Phase 0: UI/UX Simplification (IMMEDIATE - 1 week)**
|
||||
|
||||
#### 0.1 Interface Simplification
|
||||
- [ ] **Agrupar validações similares** em um único card
|
||||
- [ ] **Mostrar apenas o essencial** na visão principal
|
||||
- [ ] **Detalhes técnicos** em modal ou seção expandível
|
||||
- [ ] **Código de cores**: 🔴 Crítico, 🟡 Aviso, 🔵 Info
|
||||
- [ ] **Ícones específicos**: ⚡ CPU, 💾 Memory, 📊 Ratio
|
||||
- [ ] **Cards colapsáveis** para reduzir poluição visual
|
||||
|
||||
#### 0.2 Melhorar Hierarquia Visual
|
||||
- [ ] **Modo "Simples"** vs "Técnico"
|
||||
- [ ] **Ações diretas**: "Fix CPU Ratio" button
|
||||
- [ ] **Progress bars** para mostrar saúde do namespace
|
||||
- [ ] **Timeline** de melhorias implementadas
|
||||
- [ ] **Comparação** entre namespaces
|
||||
|
||||
#### 0.3 Funcionalidades Avançadas
|
||||
- [ ] **Botão "Apply Fix"** para ajustes automáticos
|
||||
- [ ] **Histórico de melhorias** implementadas
|
||||
- [ ] **Comparação entre namespaces**
|
||||
|
||||
---
|
||||
|
||||
### **Phase 1: Enhanced Validation & Categorization (IMMEDIATE - 1-2 weeks)**
|
||||
|
||||
#### 1.1 Smart Resource Detection
|
||||
|
||||
@@ -9,7 +9,8 @@ from fastapi.responses import FileResponse
|
||||
|
||||
from app.models.resource_models import (
|
||||
ClusterReport, NamespaceReport, ExportRequest,
|
||||
ApplyRecommendationRequest, WorkloadCategory, SmartRecommendation
|
||||
ApplyRecommendationRequest, WorkloadCategory, SmartRecommendation,
|
||||
PodHealthScore, SimplifiedValidation
|
||||
)
|
||||
from app.services.validation_service import ValidationService
|
||||
from app.services.report_service import ReportService
|
||||
@@ -802,6 +803,48 @@ async def get_resource_quotas(
|
||||
logger.error(f"Error getting resource quotas: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/pod-health-scores")
|
||||
async def get_pod_health_scores(
|
||||
namespace: Optional[str] = None,
|
||||
k8s_client=Depends(get_k8s_client)
|
||||
):
|
||||
"""Get simplified pod health scores with grouped validations"""
|
||||
try:
|
||||
# Get pods
|
||||
pods = await k8s_client.get_all_pods()
|
||||
|
||||
if namespace:
|
||||
pods = [pod for pod in pods if pod.namespace == namespace]
|
||||
|
||||
health_scores = []
|
||||
|
||||
for pod in pods:
|
||||
# Get validations for this pod
|
||||
pod_validations = validation_service.validate_pod_resources(pod)
|
||||
|
||||
# Calculate health score
|
||||
health_score = validation_service.calculate_pod_health_score(pod, pod_validations)
|
||||
health_scores.append(health_score)
|
||||
|
||||
# Sort by health score (worst first)
|
||||
health_scores.sort(key=lambda x: x.health_score)
|
||||
|
||||
return {
|
||||
"pods": health_scores,
|
||||
"total_pods": len(health_scores),
|
||||
"summary": {
|
||||
"excellent": len([h for h in health_scores if h.health_score >= 9]),
|
||||
"good": len([h for h in health_scores if 7 <= h.health_score < 9]),
|
||||
"medium": len([h for h in health_scores if 5 <= h.health_score < 7]),
|
||||
"poor": len([h for h in health_scores if 3 <= h.health_score < 5]),
|
||||
"critical": len([h for h in health_scores if h.health_score < 3])
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting pod health scores: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/health")
|
||||
async def health_check():
|
||||
"""API health check"""
|
||||
|
||||
@@ -141,6 +141,43 @@ class ResourceQuota(BaseModel):
|
||||
usage_percentage: float = 0.0
|
||||
recommended_quota: Optional[Dict[str, str]] = None
|
||||
|
||||
class PodHealthScore(BaseModel):
|
||||
"""Pod health score and simplified status"""
|
||||
pod_name: str
|
||||
namespace: str
|
||||
health_score: int # 0-10
|
||||
health_status: str # "Excellent", "Good", "Medium", "Poor", "Critical"
|
||||
status_color: str # "green", "yellow", "orange", "red"
|
||||
status_icon: str # "✅", "🟡", "🟠", "🔴"
|
||||
|
||||
# Simplified resource display
|
||||
cpu_display: str # "100m → 500m (5:1 ratio)"
|
||||
memory_display: str # "128Mi → 256Mi (2:1 ratio)"
|
||||
cpu_status: str # "✅", "⚠️", "🔴"
|
||||
memory_status: str # "✅", "⚠️", "🔴"
|
||||
|
||||
# Grouped validations
|
||||
critical_issues: List[str] = []
|
||||
warnings: List[str] = []
|
||||
info_items: List[str] = []
|
||||
|
||||
# Actions available
|
||||
available_actions: List[str] = [] # "fix_cpu_ratio", "add_requests", "add_limits"
|
||||
oc_commands: List[str] = []
|
||||
|
||||
class SimplifiedValidation(BaseModel):
|
||||
"""Simplified validation for UI display"""
|
||||
pod_name: str
|
||||
namespace: str
|
||||
validation_group: str # "cpu_ratio", "memory_ratio", "missing_requests", "missing_limits"
|
||||
severity: str # "critical", "warning", "info"
|
||||
title: str # "CPU Ratio Issue"
|
||||
description: str # "CPU ratio 5:1 exceeds recommended 3:1"
|
||||
current_value: str # "5:1"
|
||||
recommended_value: str # "3:1"
|
||||
action_required: str # "Adjust CPU limits to 300m"
|
||||
oc_command: Optional[str] = None
|
||||
|
||||
class ClusterHealth(BaseModel):
|
||||
"""Cluster health overview"""
|
||||
total_pods: int
|
||||
|
||||
@@ -12,7 +12,9 @@ from app.models.resource_models import (
|
||||
NamespaceResources,
|
||||
QoSClassification,
|
||||
ResourceQuota,
|
||||
ClusterHealth
|
||||
ClusterHealth,
|
||||
PodHealthScore,
|
||||
SimplifiedValidation
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.services.historical_analysis import HistoricalAnalysisService
|
||||
@@ -808,3 +810,227 @@ class ValidationService:
|
||||
# For now, return a simple calculation based on namespace count
|
||||
# In a real implementation, this would check actual ResourceQuota objects
|
||||
return min(len(namespaces) * 0.2, 1.0) # 20% per namespace, max 100%
|
||||
|
||||
def calculate_pod_health_score(self, pod: PodResource, validations: List[ResourceValidation]) -> PodHealthScore:
|
||||
"""Calculate pod health score and create simplified display"""
|
||||
# Calculate health score (0-10)
|
||||
health_score = 10
|
||||
|
||||
# Deduct points for issues
|
||||
for validation in validations:
|
||||
if validation.severity == "critical":
|
||||
health_score -= 3
|
||||
elif validation.severity == "error":
|
||||
health_score -= 2
|
||||
elif validation.severity == "warning":
|
||||
health_score -= 1
|
||||
|
||||
# Ensure score is between 0-10
|
||||
health_score = max(0, min(10, health_score))
|
||||
|
||||
# Determine health status and visual indicators
|
||||
if health_score >= 9:
|
||||
health_status = "Excellent"
|
||||
status_color = "green"
|
||||
status_icon = "✅"
|
||||
elif health_score >= 7:
|
||||
health_status = "Good"
|
||||
status_color = "green"
|
||||
status_icon = "✅"
|
||||
elif health_score >= 5:
|
||||
health_status = "Medium"
|
||||
status_color = "yellow"
|
||||
status_icon = "🟡"
|
||||
elif health_score >= 3:
|
||||
health_status = "Poor"
|
||||
status_color = "orange"
|
||||
status_icon = "🟠"
|
||||
else:
|
||||
health_status = "Critical"
|
||||
status_color = "red"
|
||||
status_icon = "🔴"
|
||||
|
||||
# Create simplified resource display
|
||||
cpu_display, cpu_status = self._create_cpu_display(pod)
|
||||
memory_display, memory_status = self._create_memory_display(pod)
|
||||
|
||||
# Group validations by severity
|
||||
critical_issues = []
|
||||
warnings = []
|
||||
info_items = []
|
||||
|
||||
for validation in validations:
|
||||
if validation.severity == "critical":
|
||||
critical_issues.append(validation.message)
|
||||
elif validation.severity in ["error", "warning"]:
|
||||
warnings.append(validation.message)
|
||||
else:
|
||||
info_items.append(validation.message)
|
||||
|
||||
# Determine available actions
|
||||
available_actions = self._determine_available_actions(validations)
|
||||
oc_commands = self._generate_oc_commands(pod, validations)
|
||||
|
||||
return PodHealthScore(
|
||||
pod_name=pod.name,
|
||||
namespace=pod.namespace,
|
||||
health_score=health_score,
|
||||
health_status=health_status,
|
||||
status_color=status_color,
|
||||
status_icon=status_icon,
|
||||
cpu_display=cpu_display,
|
||||
memory_display=memory_display,
|
||||
cpu_status=cpu_status,
|
||||
memory_status=memory_status,
|
||||
critical_issues=critical_issues,
|
||||
warnings=warnings,
|
||||
info_items=info_items,
|
||||
available_actions=available_actions,
|
||||
oc_commands=oc_commands
|
||||
)
|
||||
|
||||
def _create_cpu_display(self, pod: PodResource) -> tuple[str, str]:
|
||||
"""Create CPU display string and status"""
|
||||
if pod.cpu_requests == 0 and pod.cpu_limits == 0:
|
||||
return "No CPU resources defined", "🔴"
|
||||
|
||||
# Format CPU values
|
||||
cpu_req_str = self._format_cpu_value(pod.cpu_requests)
|
||||
cpu_lim_str = self._format_cpu_value(pod.cpu_limits)
|
||||
|
||||
# Calculate ratio
|
||||
if pod.cpu_requests > 0:
|
||||
ratio = pod.cpu_limits / pod.cpu_requests
|
||||
ratio_str = f"({ratio:.1f}:1 ratio)"
|
||||
else:
|
||||
ratio_str = "(no requests)"
|
||||
|
||||
display = f"{cpu_req_str} → {cpu_lim_str} {ratio_str}"
|
||||
|
||||
# Determine status
|
||||
if pod.cpu_requests == 0:
|
||||
status = "🔴" # No requests
|
||||
elif pod.cpu_limits == 0:
|
||||
status = "🟡" # No limits
|
||||
elif pod.cpu_requests > 0 and pod.cpu_limits > 0:
|
||||
ratio = pod.cpu_limits / pod.cpu_requests
|
||||
if ratio > 5:
|
||||
status = "🔴" # Very high ratio
|
||||
elif ratio > 3:
|
||||
status = "🟡" # High ratio
|
||||
else:
|
||||
status = "✅" # Good ratio
|
||||
else:
|
||||
status = "🔴"
|
||||
|
||||
return display, status
|
||||
|
||||
def _create_memory_display(self, pod: PodResource) -> tuple[str, str]:
|
||||
"""Create memory display string and status"""
|
||||
if pod.memory_requests == 0 and pod.memory_limits == 0:
|
||||
return "No memory resources defined", "🔴"
|
||||
|
||||
# Format memory values
|
||||
mem_req_str = self._format_memory_value(pod.memory_requests)
|
||||
mem_lim_str = self._format_memory_value(pod.memory_limits)
|
||||
|
||||
# Calculate ratio
|
||||
if pod.memory_requests > 0:
|
||||
ratio = pod.memory_limits / pod.memory_requests
|
||||
ratio_str = f"({ratio:.1f}:1 ratio)"
|
||||
else:
|
||||
ratio_str = "(no requests)"
|
||||
|
||||
display = f"{mem_req_str} → {mem_lim_str} {ratio_str}"
|
||||
|
||||
# Determine status
|
||||
if pod.memory_requests == 0:
|
||||
status = "🔴" # No requests
|
||||
elif pod.memory_limits == 0:
|
||||
status = "🟡" # No limits
|
||||
elif pod.memory_requests > 0 and pod.memory_limits > 0:
|
||||
ratio = pod.memory_limits / pod.memory_requests
|
||||
if ratio > 5:
|
||||
status = "🔴" # Very high ratio
|
||||
elif ratio > 3:
|
||||
status = "🟡" # High ratio
|
||||
else:
|
||||
status = "✅" # Good ratio
|
||||
else:
|
||||
status = "🔴"
|
||||
|
||||
return display, status
|
||||
|
||||
def _format_cpu_value(self, value: float) -> str:
|
||||
"""Format CPU value for display"""
|
||||
if value >= 1.0:
|
||||
return f"{value:.1f} cores"
|
||||
else:
|
||||
return f"{int(value * 1000)}m"
|
||||
|
||||
def _format_memory_value(self, value_bytes: float) -> str:
|
||||
"""Format memory value for display"""
|
||||
if value_bytes >= 1024 * 1024 * 1024: # >= 1 GiB
|
||||
return f"{value_bytes / (1024 * 1024 * 1024):.1f} GiB"
|
||||
else:
|
||||
return f"{int(value_bytes / (1024 * 1024))} MiB"
|
||||
|
||||
def _determine_available_actions(self, validations: List[ResourceValidation]) -> List[str]:
|
||||
"""Determine available actions based on validations"""
|
||||
actions = []
|
||||
|
||||
for validation in validations:
|
||||
if validation.validation_type == "missing_requests":
|
||||
actions.append("add_requests")
|
||||
elif validation.validation_type == "missing_limits":
|
||||
actions.append("add_limits")
|
||||
elif validation.validation_type == "cpu_ratio":
|
||||
actions.append("fix_cpu_ratio")
|
||||
elif validation.validation_type == "memory_ratio":
|
||||
actions.append("fix_memory_ratio")
|
||||
|
||||
return list(set(actions)) # Remove duplicates
|
||||
|
||||
def _generate_oc_commands(self, pod: PodResource, validations: List[ResourceValidation]) -> List[str]:
|
||||
"""Generate oc commands for fixing issues"""
|
||||
commands = []
|
||||
|
||||
# Generate commands for each validation
|
||||
for validation in validations:
|
||||
if validation.validation_type == "missing_requests":
|
||||
cmd = self._generate_add_requests_command(pod, validation)
|
||||
if cmd:
|
||||
commands.append(cmd)
|
||||
elif validation.validation_type == "missing_limits":
|
||||
cmd = self._generate_add_limits_command(pod, validation)
|
||||
if cmd:
|
||||
commands.append(cmd)
|
||||
elif validation.validation_type in ["cpu_ratio", "memory_ratio"]:
|
||||
cmd = self._generate_fix_ratio_command(pod, validation)
|
||||
if cmd:
|
||||
commands.append(cmd)
|
||||
|
||||
return commands
|
||||
|
||||
def _generate_add_requests_command(self, pod: PodResource, validation: ResourceValidation) -> str:
|
||||
"""Generate oc command to add requests"""
|
||||
# This would need to be implemented based on specific container
|
||||
return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"requests\":{{\"cpu\":\"100m\",\"memory\":\"128Mi\"}}}}}}]}}}}'"
|
||||
|
||||
def _generate_add_limits_command(self, pod: PodResource, validation: ResourceValidation) -> str:
|
||||
"""Generate oc command to add limits"""
|
||||
return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"limits\":{{\"cpu\":\"500m\",\"memory\":\"512Mi\"}}}}}}]}}}}'"
|
||||
|
||||
def _generate_fix_ratio_command(self, pod: PodResource, validation: ResourceValidation) -> str:
|
||||
"""Generate oc command to fix ratio"""
|
||||
# Calculate recommended limits based on 3:1 ratio
|
||||
if validation.validation_type == "cpu_ratio":
|
||||
recommended_limit = pod.cpu_requests * 3
|
||||
limit_str = self._format_cpu_value(recommended_limit)
|
||||
return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"limits\":{{\"cpu\":\"{limit_str}\"}}}}}}]}}}}'"
|
||||
elif validation.validation_type == "memory_ratio":
|
||||
recommended_limit = pod.memory_requests * 3
|
||||
limit_str = self._format_memory_value(recommended_limit)
|
||||
return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"limits\":{{\"memory\":\"{limit_str}\"}}}}}}]}}}}'"
|
||||
|
||||
return ""
|
||||
|
||||
@@ -1278,6 +1278,336 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Simplified View Styles */
|
||||
.view-mode-toggle {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: 2px solid #e9ecef;
|
||||
background: white;
|
||||
color: #6c757d;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.mode-btn:hover:not(.active) {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.health-summary {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 2px solid;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.stat-card.excellent {
|
||||
border-color: #28a745;
|
||||
background: #f8fff9;
|
||||
}
|
||||
|
||||
.stat-card.good {
|
||||
border-color: #28a745;
|
||||
background: #f8fff9;
|
||||
}
|
||||
|
||||
.stat-card.medium {
|
||||
border-color: #ffc107;
|
||||
background: #fffdf5;
|
||||
}
|
||||
|
||||
.stat-card.poor {
|
||||
border-color: #fd7e14;
|
||||
background: #fff8f5;
|
||||
}
|
||||
|
||||
.stat-card.critical {
|
||||
border-color: #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.pod-cards-container {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pod-health-card {
|
||||
background: white;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pod-health-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.pod-health-card.excellent {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.pod-health-card.good {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.pod-health-card.medium {
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.pod-health-card.poor {
|
||||
border-color: #fd7e14;
|
||||
}
|
||||
|
||||
.pod-health-card.critical {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.pod-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pod-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pod-namespace {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.health-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.health-score-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.resource-display {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.resource-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.resource-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.resource-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pod-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #007bff;
|
||||
background: white;
|
||||
color: #007bff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.technical-details {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.technical-details.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.technical-card {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.technical-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.validation-groups {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.validation-group {
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.validation-group.critical {
|
||||
background: #fff5f5;
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.validation-group.warning {
|
||||
background: #fffdf5;
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.validation-group.info {
|
||||
background: #f8f9fa;
|
||||
border-left-color: #6c757d;
|
||||
}
|
||||
|
||||
.validation-group-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.validation-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.validation-item {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.validation-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.oc-command {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1291,6 +1621,10 @@
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span class="nav-text">Cluster Health</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-section="simplified-view">
|
||||
<span class="nav-icon">🎯</span>
|
||||
<span class="nav-text">Simplified View</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-section="workload-categories">
|
||||
<span class="nav-icon">📊</span>
|
||||
<span class="nav-text">Workload Analysis</span>
|
||||
@@ -1561,6 +1895,70 @@
|
||||
<div id="vpaList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Simplified View -->
|
||||
<div class="card" id="simplifiedViewCard" style="display: none;">
|
||||
<h2>🎯 Simplified Pod Health View</h2>
|
||||
|
||||
<!-- View Mode Toggle -->
|
||||
<div class="view-mode-toggle">
|
||||
<button class="mode-btn active" id="simpleModeBtn" onclick="switchViewMode('simple')">Simple Mode</button>
|
||||
<button class="mode-btn" id="technicalModeBtn" onclick="switchViewMode('technical')">Technical Mode</button>
|
||||
</div>
|
||||
|
||||
<!-- Health Summary -->
|
||||
<div class="health-summary" id="healthSummary">
|
||||
<div class="summary-stats">
|
||||
<div class="stat-card excellent">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="excellentCount">0</div>
|
||||
<div class="stat-label">Excellent</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card good">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="goodCount">0</div>
|
||||
<div class="stat-label">Good</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card medium">
|
||||
<div class="stat-icon">🟡</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="mediumCount">0</div>
|
||||
<div class="stat-label">Medium</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card poor">
|
||||
<div class="stat-icon">🟠</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="poorCount">0</div>
|
||||
<div class="stat-label">Poor</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card critical">
|
||||
<div class="stat-icon">🔴</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="criticalCount">0</div>
|
||||
<div class="stat-label">Critical</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pod Health Cards (Simple Mode) -->
|
||||
<div id="simplePodCards" class="pod-cards-container">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Technical Details (Technical Mode) -->
|
||||
<div id="technicalDetails" class="technical-details hidden">
|
||||
<div id="technicalPodCards" class="technical-cards-container">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="loading hidden" id="loading">
|
||||
<p>Loading data...</p>
|
||||
@@ -3422,6 +3820,7 @@
|
||||
// Show selected section
|
||||
const sectionMap = {
|
||||
'dashboard': 'validationsCard',
|
||||
'simplified-view': 'simplifiedViewCard',
|
||||
'historical-analysis': 'historicalCard',
|
||||
'smart-recommendations': 'smartRecommendationsCard',
|
||||
'workload-categories': 'workloadCategoriesCard',
|
||||
@@ -3441,21 +3840,208 @@
|
||||
case 'dashboard':
|
||||
loadClusterHealth();
|
||||
break;
|
||||
case 'historical-analysis':
|
||||
loadHistoricalValidations();
|
||||
break;
|
||||
case 'smart-recommendations':
|
||||
loadSmartRecommendations();
|
||||
break;
|
||||
case 'workload-categories':
|
||||
loadWorkloadCategories();
|
||||
break;
|
||||
case 'vpa-recommendations':
|
||||
loadVPARecommendations();
|
||||
break;
|
||||
case 'simplified-view':
|
||||
loadSimplifiedView();
|
||||
break;
|
||||
case 'historical-analysis':
|
||||
loadHistoricalValidations();
|
||||
break;
|
||||
case 'smart-recommendations':
|
||||
loadSmartRecommendations();
|
||||
break;
|
||||
case 'workload-categories':
|
||||
loadWorkloadCategories();
|
||||
break;
|
||||
case 'vpa-recommendations':
|
||||
loadVPARecommendations();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified View Functions
|
||||
let currentViewMode = 'simple';
|
||||
let healthScoresData = null;
|
||||
|
||||
async function loadSimplifiedView() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const response = await fetch('/api/v1/pod-health-scores');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
healthScoresData = await response.json();
|
||||
updateHealthSummary(healthScoresData.summary);
|
||||
updatePodCards(healthScoresData.pods);
|
||||
|
||||
hideLoading();
|
||||
} catch (error) {
|
||||
console.error('Error loading simplified view:', error);
|
||||
showError('Error loading simplified view: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateHealthSummary(summary) {
|
||||
document.getElementById('excellentCount').textContent = summary.excellent;
|
||||
document.getElementById('goodCount').textContent = summary.good;
|
||||
document.getElementById('mediumCount').textContent = summary.medium;
|
||||
document.getElementById('poorCount').textContent = summary.poor;
|
||||
document.getElementById('criticalCount').textContent = summary.critical;
|
||||
}
|
||||
|
||||
function updatePodCards(pods) {
|
||||
const container = document.getElementById('simplePodCards');
|
||||
const technicalContainer = document.getElementById('technicalPodCards');
|
||||
|
||||
let simpleHtml = '';
|
||||
let technicalHtml = '';
|
||||
|
||||
pods.forEach(pod => {
|
||||
// Simple mode card
|
||||
simpleHtml += `
|
||||
<div class="pod-health-card ${pod.status_color}">
|
||||
<div class="pod-header">
|
||||
<div>
|
||||
<div class="pod-name">${pod.pod_name}</div>
|
||||
<div class="pod-namespace">${pod.namespace}</div>
|
||||
</div>
|
||||
<div class="health-score">
|
||||
<span class="health-score-value">${pod.health_score}/10</span>
|
||||
<span class="health-status">${pod.health_status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="resource-display">
|
||||
<div class="resource-item">
|
||||
<span class="resource-icon">⚡</span>
|
||||
<div class="resource-text">
|
||||
<div class="resource-label">CPU</div>
|
||||
<div class="resource-value">${pod.cpu_display} ${pod.cpu_status}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="resource-icon">💾</span>
|
||||
<div class="resource-text">
|
||||
<div class="resource-label">Memory</div>
|
||||
<div class="resource-value">${pod.memory_display} ${pod.memory_status}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pod-actions">
|
||||
<button class="action-btn primary" onclick="viewPodDetails('${pod.pod_name}')">View Details</button>
|
||||
${pod.available_actions.includes('fix_cpu_ratio') ? '<button class="action-btn" onclick="fixPodIssue(\'' + pod.pod_name + '\', \'cpu_ratio\')">Fix CPU Ratio</button>' : ''}
|
||||
${pod.available_actions.includes('add_requests') ? '<button class="action-btn" onclick="fixPodIssue(\'' + pod.pod_name + '\', \'add_requests\')">Add Requests</button>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Technical mode card
|
||||
technicalHtml += `
|
||||
<div class="technical-card">
|
||||
<div class="technical-header">
|
||||
<div>
|
||||
<div class="pod-name">${pod.pod_name}</div>
|
||||
<div class="pod-namespace">${pod.namespace}</div>
|
||||
</div>
|
||||
<div class="health-score">
|
||||
<span class="health-score-value">${pod.health_score}/10</span>
|
||||
<span class="health-status">${pod.health_status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="validation-groups">
|
||||
${pod.critical_issues.length > 0 ? `
|
||||
<div class="validation-group critical">
|
||||
<div class="validation-group-title">🔴 Critical Issues (${pod.critical_issues.length})</div>
|
||||
<ul class="validation-list">
|
||||
${pod.critical_issues.map(issue => `<li class="validation-item">${issue}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${pod.warnings.length > 0 ? `
|
||||
<div class="validation-group warning">
|
||||
<div class="validation-group-title">🟡 Warnings (${pod.warnings.length})</div>
|
||||
<ul class="validation-list">
|
||||
${pod.warnings.map(warning => `<li class="validation-item">${warning}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${pod.info_items.length > 0 ? `
|
||||
<div class="validation-group info">
|
||||
<div class="validation-group-title">🔵 Information (${pod.info_items.length})</div>
|
||||
<ul class="validation-list">
|
||||
${pod.info_items.map(info => `<li class="validation-item">${info}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${pod.oc_commands.length > 0 ? `
|
||||
<div class="oc-commands">
|
||||
<h4>Available Commands:</h4>
|
||||
${pod.oc_commands.map(cmd => `
|
||||
<div class="oc-command">
|
||||
${cmd}
|
||||
<button class="copy-btn" onclick="copyToClipboard('${cmd.replace(/'/g, "\\'")}')">Copy</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = simpleHtml;
|
||||
technicalContainer.innerHTML = technicalHtml;
|
||||
}
|
||||
|
||||
function switchViewMode(mode) {
|
||||
currentViewMode = mode;
|
||||
|
||||
// Update button states
|
||||
document.getElementById('simpleModeBtn').classList.toggle('active', mode === 'simple');
|
||||
document.getElementById('technicalModeBtn').classList.toggle('active', mode === 'technical');
|
||||
|
||||
// Show/hide appropriate content
|
||||
document.getElementById('simplePodCards').style.display = mode === 'simple' ? 'block' : 'none';
|
||||
document.getElementById('technicalDetails').classList.toggle('hidden', mode === 'simple');
|
||||
}
|
||||
|
||||
function viewPodDetails(podName) {
|
||||
// Switch to technical mode and scroll to pod
|
||||
switchViewMode('technical');
|
||||
const podCard = document.querySelector(`[data-pod="${podName}"]`);
|
||||
if (podCard) {
|
||||
podCard.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function fixPodIssue(podName, issueType) {
|
||||
// This would implement the fix logic
|
||||
alert(`Fixing ${issueType} for pod ${podName}`);
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Show success feedback
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#28a745';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.style.background = '#28a745';
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('Failed to copy to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
// Add click handlers for navigation
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
|
||||
Reference in New Issue
Block a user