Files
openshift-resource-governance/app/static/index.html
andersonid 162af739e4 Fix Namespace Analysis code duplication
- Remove duplicate createNamespaceDetails() function
- Fix validation.rule_name to validation.validation_type
- Keep only showNamespaceDetailsSimple() function
- Eliminate redundant code in namespace analysis modal
- Improve code maintainability and reduce duplication
2025-10-01 16:12:21 -03:00

1798 lines
65 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenShift Resource Governance Tool</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
display: flex;
min-height: 100vh;
}
/* Sidebar Styles */
.sidebar {
width: 250px;
background: #2c3e50;
color: white;
position: fixed;
height: 100vh;
left: 0;
top: 0;
z-index: 1000;
overflow-y: auto;
transition: transform 0.3s ease;
}
.sidebar-header {
padding: 1.5rem 1rem;
border-bottom: 1px solid #34495e;
background: #34495e;
}
.sidebar-header h2 {
font-size: 1.2rem;
font-weight: 600;
}
.sidebar-nav {
padding: 1rem 0;
}
.nav-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
color: #bdc3c7;
text-decoration: none;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.nav-item:hover {
background: #34495e;
color: white;
border-left-color: #3498db;
}
.nav-item.active {
background: #3498db;
color: white;
border-left-color: #2980b9;
}
.nav-divider {
height: 1px;
background-color: #34495e;
margin: 0.5rem 1rem;
}
.nav-icon {
font-size: 1.2rem;
margin-right: 0.75rem;
width: 20px;
text-align: center;
}
.nav-text {
font-weight: 500;
}
/* Main Content */
.main-content {
flex: 1;
margin-left: 250px;
min-height: 100vh;
}
/* Export Button */
.export-button {
position: fixed;
top: 1rem;
right: 1rem;
background: #27ae60;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
z-index: 1001;
transition: background 0.3s ease;
}
.export-button:hover {
background: #229954;
}
/* Header */
.header {
background: white;
padding: 2rem;
border-bottom: 1px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 2rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 0.5rem;
}
/* Content */
.content {
padding: 2rem;
}
/* Cards */
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #e0e0e0;
}
.card h2 {
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 1rem;
}
.card h3 {
font-size: 1.2rem;
font-weight: 600;
color: #34495e;
margin-bottom: 0.75rem;
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.metric-card {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
border-left: 4px solid #3498db;
}
.metric-value {
font-size: 2rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.metric-label {
font-size: 0.9rem;
color: #7f8c8d;
font-weight: 500;
}
.metric-card.warning {
border-left-color: #f39c12;
}
.metric-card.error {
border-left-color: #e74c3c;
}
.metric-card.success {
border-left-color: #27ae60;
}
.info-icon {
cursor: pointer;
color: #3498db;
font-size: 14px;
margin-left: 5px;
display: inline-block;
}
.info-icon:hover {
color: #2980b9;
}
.overcommit-details {
padding: 1rem;
}
.overcommit-details h3 {
color: #2c3e50;
margin-bottom: 1rem;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
}
.metric-detail {
margin: 0.75rem 0;
padding: 0.5rem;
background: #f8f9fa;
border-left: 3px solid #3498db;
border-radius: 4px;
}
.metric-detail strong {
color: #2c3e50;
}
/* Status Overview */
.status-overview {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
}
.status-icon {
font-size: 3rem;
}
.status-content h3 {
margin-bottom: 0.5rem;
font-size: 1.5rem;
}
.status-content p {
color: #7f8c8d;
font-size: 1rem;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 0;
border: 1px solid #888;
border-radius: 8px;
width: 80%;
max-width: 1000px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
background: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #dee2e6;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
color: #495057;
}
.modal-body {
padding: 20px;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
/* Namespace Details Styles */
.namespace-details {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.namespace-summary {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.namespace-summary ul {
margin: 10px 0;
padding-left: 20px;
}
.pod-detail {
border: 1px solid #dee2e6;
border-radius: 5px;
margin: 15px 0;
padding: 15px;
background: #fff;
}
.pod-detail h5 {
color: #495057;
margin-bottom: 10px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 5px;
}
.containers-detail {
margin: 15px 0;
}
.container-detail {
background: #f8f9fa;
padding: 10px;
margin: 10px 0;
border-radius: 3px;
border-left: 4px solid #007bff;
}
.validations-detail {
margin: 15px 0;
}
.validation-item {
padding: 10px;
margin: 8px 0;
border-radius: 3px;
border-left: 4px solid #6c757d;
}
.validation-item.severity-error {
background: #f8d7da;
border-left-color: #dc3545;
}
.validation-item.severity-warning {
background: #fff3cd;
border-left-color: #ffc107;
}
.validation-item.severity-info {
background: #d1ecf1;
border-left-color: #17a2b8;
}
.validation-item p {
margin: 5px 0;
}
.validation-item strong {
color: #495057;
}
.validation-item em {
color: #6c757d;
font-size: 0.9em;
}
/* Fix Modal Styles */
.fix-details {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.fix-info {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.fix-info ul {
margin: 10px 0;
padding-left: 20px;
}
.fix-actions {
margin-top: 20px;
text-align: right;
}
.workload-selector {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.workload-selector select {
margin: 0 10px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.chart-container {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
}
.chart-container canvas {
border: 1px solid #eee;
border-radius: 4px;
}
.cluster-stats {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
text-align: center;
flex: 1;
}
.stat-card h4 {
margin: 0 0 10px 0;
color: #495057;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.metric-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
}
.metric-section h4 {
margin: 0 0 20px 0;
color: #495057;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
}
.metric-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f8f9fa;
}
.metric-row:last-child {
border-bottom: none;
}
.metric-label {
font-weight: 500;
color: #6c757d;
min-width: 120px;
}
.metric-value {
font-weight: bold;
color: #212529;
font-family: 'Courier New', monospace;
}
.metric-percent {
color: #6c757d;
font-size: 12px;
font-style: italic;
}
@media (max-width: 768px) {
.metrics-grid {
grid-template-columns: 1fr;
}
.cluster-stats {
flex-direction: column;
}
}
/* Problem Summary Table */
.problem-summary {
margin-bottom: 2rem;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
align-items: center;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-weight: 500;
color: #34495e;
}
.filter-group select,
.filter-group input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
}
.apply-filters-btn {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s ease;
}
.apply-filters-btn:hover {
background: #2980b9;
}
/* Table Styles */
.table-container {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
th {
background: #f8f9fa;
padding: 1rem;
text-align: left;
font-weight: 600;
color: #2c3e50;
border-bottom: 2px solid #e0e0e0;
}
td {
padding: 1rem;
border-bottom: 1px solid #f0f0f0;
}
tr:hover {
background: #f8f9fa;
}
/* Severity Badges */
.severity-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.severity-critical {
background: #ffebee;
color: #c62828;
}
.severity-error {
background: #ffebee;
color: #d32f2f;
}
.severity-warning {
background: #fff3e0;
color: #f57c00;
}
.severity-info {
background: #e3f2fd;
color: #1976d2;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.8rem;
}
/* Quick Actions */
.quick-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.quick-action-btn {
background: #34495e;
color: white;
border: none;
padding: 1rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.75rem;
}
.quick-action-btn:hover {
background: #2c3e50;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Loading States */
.loading {
text-align: center;
padding: 2rem;
color: #7f8c8d;
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 0.5rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.main-content {
margin-left: 0;
}
.metrics-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.filters {
flex-direction: column;
align-items: stretch;
}
.quick-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h2>Resource Governance</h2>
</div>
<nav class="sidebar-nav">
<a href="#" class="nav-item active" data-section="dashboard">
<span class="nav-icon">🏠</span>
<span class="nav-text">Dashboard</span>
</a>
<div class="nav-divider"></div>
<a href="#" class="nav-item" data-section="smart-recommendations">
<span class="nav-icon">🎯</span>
<span class="nav-text">Smart Recommendations</span>
</a>
<a href="#" class="nav-item" data-section="vpa-management">
<span class="nav-icon">⚙️</span>
<span class="nav-text">VPA Management</span>
</a>
<a href="#" class="nav-item" data-section="historical-analysis">
<span class="nav-icon">📈</span>
<span class="nav-text">Historical Analysis</span>
</a>
</nav>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Export Button -->
<button class="export-button" onclick="exportData()">Export</button>
<!-- Header -->
<div class="header">
<h1>OpenShift Resource Governance Tool</h1>
</div>
<!-- Content -->
<div class="content">
<!-- Dashboard Section -->
<div id="dashboardSection">
<!-- Cluster Overview -->
<div class="card">
<h2>🎯 Resource Governance Dashboard</h2>
<!-- Status Overview -->
<div class="status-overview">
<div class="status-icon" id="clusterStatusIcon">🟢</div>
<div class="status-content">
<h3 id="clusterStatus">Cluster Healthy</h3>
<p id="clusterStatusMessage">All systems operational</p>
</div>
</div>
<!-- Metrics Grid -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value" id="totalPods">-</div>
<div class="metric-label">Total Pods</div>
</div>
<div class="metric-card">
<div class="metric-value" id="totalNamespaces">-</div>
<div class="metric-label">Namespaces</div>
</div>
<div class="metric-card">
<div class="metric-value" id="totalNodes">-</div>
<div class="metric-label">Nodes</div>
</div>
<div class="metric-card error" id="criticalIssuesCard">
<div class="metric-value" id="criticalIssues">-</div>
<div class="metric-label">Critical Issues</div>
</div>
</div>
<!-- Overcommit Summary -->
<div class="card">
<h3>📊 Cluster Overcommit Summary</h3>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value" id="cpuOvercommit">-</div>
<div class="metric-label">CPU Overcommit <span class="info-icon" onclick="showOvercommitDetails('cpu')"></span></div>
</div>
<div class="metric-card">
<div class="metric-value" id="memoryOvercommit">-</div>
<div class="metric-label">Memory Overcommit <span class="info-icon" onclick="showOvercommitDetails('memory')"></span></div>
</div>
<div class="metric-card">
<div class="metric-value" id="namespacesInOvercommit">-</div>
<div class="metric-label">Namespaces in Overcommit</div>
</div>
<div class="metric-card">
<div class="metric-value" id="resourceUtilization">-</div>
<div class="metric-label">Resource Utilization <span class="info-icon" onclick="showResourceUtilizationDetails()"></span></div>
</div>
</div>
</div>
</div>
<!-- Problem Summary -->
<div class="card problem-summary">
<h2>🔍 Problem Summary</h2>
<p style="margin-bottom: 1.5rem; color: #7f8c8d;">Identify namespaces with resource configuration issues and take action</p>
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label for="severityFilter">Severity:</label>
<select id="severityFilter">
<option value="all">All</option>
<option value="critical">Critical</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</div>
<div class="filter-group">
<label for="perPageFilter">Per page:</label>
<select id="perPageFilter">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="filter-checkbox">
<input type="checkbox" id="includeSystemNamespaces">
<label for="includeSystemNamespaces">Include system namespaces</label>
</div>
<button class="apply-filters-btn" onclick="applyFilters()">Apply Filters</button>
</div>
<!-- Problem Table -->
<div class="table-container">
<table>
<thead>
<tr>
<th>Namespace</th>
<th>Pods</th>
<th>Issues</th>
<th>Severity</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="problemTableBody">
<tr>
<td colspan="5" class="loading">Loading data...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<h2>⚡ Quick Actions</h2>
<div class="quick-actions">
<button class="quick-action-btn" onclick="generateVPARecommendations()">
<span>🎯</span>
<span>Generate VPA Recommendations</span>
</button>
<button class="quick-action-btn" onclick="showHistoricalAnalysis()">
<span>📈</span>
<span>Historical Analysis</span>
</button>
<button class="quick-action-btn" onclick="exportComplianceReport()">
<span>📊</span>
<span>Export Compliance Report</span>
</button>
<button class="quick-action-btn" onclick="fixAllIssues()">
<span>🔧</span>
<span>Fix All Issues</span>
</button>
</div>
</div>
</div>
<!-- Smart Recommendations Section -->
<div id="smartRecommendationsSection" style="display: none;">
<div class="card">
<h2>🎯 Smart Recommendations</h2>
<div id="smartRecommendationsContent">
<p class="loading">Loading recommendations...</p>
</div>
</div>
</div>
<!-- VPA Management Section -->
<div id="vpaManagementSection" style="display: none;">
<div class="card">
<h2>⚙️ VPA Management</h2>
<div id="vpaManagementContent">
<p class="loading">Loading VPA data...</p>
</div>
</div>
</div>
<!-- Historical Analysis Section -->
<div id="historicalAnalysisSection" style="display: none;">
<div class="card">
<h2>📈 Historical Analysis</h2>
<div id="historicalAnalysisContent">
<p class="loading">Loading historical data...</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variables
let currentData = null;
let currentFilters = {
severity: 'all',
perPage: 20,
includeSystem: false
};
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
loadDashboard();
setupEventListeners();
});
// Setup event listeners
function setupEventListeners() {
// Navigation
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const section = this.dataset.section;
showSection(section);
});
});
// Filter changes
document.getElementById('severityFilter').addEventListener('change', function() {
currentFilters.severity = this.value;
});
document.getElementById('perPageFilter').addEventListener('change', function() {
currentFilters.perPage = parseInt(this.value);
});
document.getElementById('includeSystemNamespaces').addEventListener('change', function() {
currentFilters.includeSystem = this.checked;
});
}
// Show specific section
function showSection(section) {
// Hide all sections
document.querySelectorAll('[id$="Section"]').forEach(sec => {
sec.style.display = 'none';
});
// Remove active class from all nav items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
// Show selected section
document.getElementById(section + 'Section').style.display = 'block';
document.querySelector(`[data-section="${section}"]`).classList.add('active');
// Load section data
switch(section) {
case 'dashboard':
loadDashboard();
break;
case 'smart-recommendations':
loadSmartRecommendations();
break;
case 'vpa-management':
loadVPAManagement();
break;
case 'historical-analysis':
loadHistoricalAnalysis();
break;
}
}
// Load dashboard data
async function loadDashboard() {
try {
showLoading();
const response = await fetch('/api/v1/cluster/status');
const data = await response.json();
currentData = data;
updateDashboard(data);
} catch (error) {
console.error('Error loading dashboard:', error);
showError('Failed to load dashboard data');
}
}
// Update dashboard with data
function updateDashboard(data) {
// Update metrics
document.getElementById('totalPods').textContent = data.total_pods || 0;
document.getElementById('totalNamespaces').textContent = data.total_namespaces || 0;
document.getElementById('totalNodes').textContent = data.total_nodes || 0;
document.getElementById('criticalIssues').textContent = data.critical_issues || 0;
// Update overcommit metrics
if (data.overcommit) {
document.getElementById('cpuOvercommit').textContent = `${data.overcommit.cpu_overcommit_percent}%`;
document.getElementById('memoryOvercommit').textContent = `${data.overcommit.memory_overcommit_percent}%`;
document.getElementById('namespacesInOvercommit').textContent = data.overcommit.namespaces_in_overcommit || 0;
// Calculate resource utilization (usage vs requests)
const resourceUtilization = data.overcommit.resource_utilization || 0;
document.getElementById('resourceUtilization').textContent = `${resourceUtilization}%`;
// Store overcommit data for modal display
window.overcommitData = data.overcommit;
} else {
document.getElementById('cpuOvercommit').textContent = '0%';
document.getElementById('memoryOvercommit').textContent = '0%';
document.getElementById('namespacesInOvercommit').textContent = '0';
document.getElementById('resourceUtilization').textContent = '0%';
}
// Update status
const statusIcon = document.getElementById('clusterStatusIcon');
const statusText = document.getElementById('clusterStatus');
const statusMessage = document.getElementById('clusterStatusMessage');
if (data.critical_issues > 0) {
statusIcon.textContent = '🔴';
statusText.textContent = 'Critical Issues Found';
statusMessage.textContent = `${data.critical_issues} critical issues need attention`;
} else if (data.total_warnings > 0) {
statusIcon.textContent = '🟡';
statusText.textContent = 'Issues Found';
statusMessage.textContent = `${data.total_warnings} warnings found`;
} else {
statusIcon.textContent = '🟢';
statusText.textContent = 'Cluster Healthy';
statusMessage.textContent = 'All systems operational';
}
// Update problem table
updateProblemTable(data);
}
// Update problem table
function updateProblemTable(data) {
const tbody = document.getElementById('problemTableBody');
if (!data.namespaces || data.namespaces.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #7f8c8d;">No data available</td></tr>';
return;
}
let html = '';
data.namespaces.forEach(namespace => {
const severity = getHighestSeverity(namespace);
const severityClass = `severity-${severity}`;
html += `
<tr>
<td><strong>${namespace.namespace}</strong></td>
<td>${Object.keys(namespace.pods || {}).length}</td>
<td>${namespace.total_validations || 0}</td>
<td><span class="severity-badge ${severityClass}">${severity}</span></td>
<td>
<div class="action-buttons">
<button class="btn btn-primary btn-sm" onclick="analyzeNamespace('${namespace.namespace}')">Analyze</button>
<button class="btn btn-success btn-sm" onclick="fixNamespace('${namespace.namespace}')">Fix</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
// Get highest severity from namespace
function getHighestSeverity(namespace) {
const breakdown = namespace.severity_breakdown || {};
if (breakdown.error > 0) return 'error';
if (breakdown.warning > 0) return 'warning';
if (breakdown.info > 0) return 'info';
return 'info';
}
// Analyze namespace - show detailed issues
function analyzeNamespace(namespaceName) {
if (!currentData || !currentData.namespaces) return;
const namespace = currentData.namespaces.find(ns => ns.namespace === namespaceName);
if (!namespace) return;
// Show details in modal
showNamespaceDetailsSimple(namespaceName);
}
// Show namespace details in modal
function showNamespaceDetailsSimple(namespaceName) {
// Create modal if it doesn't exist
let modal = document.getElementById('namespaceModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'namespaceModal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>📋 Namespace Analysis</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="modalBody"></div>
</div>
`;
document.body.appendChild(modal);
// Add close functionality
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
}
// Create detailed content
const namespace = currentData.namespaces.find(ns => ns.namespace === namespaceName);
if (!namespace) return;
let content = `
<div class="namespace-details">
<h3>📋 ${namespaceName} - Detailed Analysis</h3>
<div class="namespace-summary">
<p><strong>Pods:</strong> ${Object.keys(namespace.pods || {}).length}</p>
<p><strong>Total Issues:</strong> ${namespace.total_validations || 0}</p>
<p><strong>Severity Breakdown:</strong></p>
<ul>
<li>Errors: ${namespace.severity_breakdown?.error || 0}</li>
<li>Warnings: ${namespace.severity_breakdown?.warning || 0}</li>
<li>Info: ${namespace.severity_breakdown?.info || 0}</li>
</ul>
</div>
<div class="pods-details">
<h4>🔍 Pod Analysis</h4>
`;
// Add details for each pod
Object.values(namespace.pods || {}).forEach(pod => {
content += `
<div class="pod-detail">
<h5>📦 ${pod.pod_name}</h5>
<p><strong>Status:</strong> ${pod.phase}</p>
<p><strong>Node:</strong> ${pod.node_name}</p>
<div class="containers-detail">
<h6>Containers:</h6>
`;
pod.containers.forEach(container => {
const hasRequests = Object.keys(container.resources?.requests || {}).length > 0;
const hasLimits = Object.keys(container.resources?.limits || {}).length > 0;
content += `
<div class="container-detail">
<p><strong>${container.name}</strong> (${container.image})</p>
<p>Requests: ${hasRequests ? JSON.stringify(container.resources.requests) : '❌ Not defined'}</p>
<p>Limits: ${hasLimits ? JSON.stringify(container.resources.limits) : '❌ Not defined'}</p>
</div>
`;
});
content += `
</div>
<div class="validations-detail">
<h6>Issues Found:</h6>
`;
if (pod.validations && pod.validations.length > 0) {
pod.validations.forEach(validation => {
const severityClass = `severity-${validation.severity}`;
content += `
<div class="validation-item ${severityClass}">
<p><strong>${validation.validation_type}:</strong> ${validation.message}</p>
<p><em>Recommendation:</em> ${validation.recommendation}</p>
</div>
`;
});
} else {
content += `<p>No issues found for this pod.</p>`;
}
content += `
</div>
</div>
`;
});
content += `
</div>
</div>
`;
// Populate and show modal
document.getElementById('modalBody').innerHTML = content;
modal.style.display = 'block';
}
// Fix namespace - placeholder for now
function fixNamespace(namespaceName) {
// Create modal if it doesn't exist
let modal = document.getElementById('fixModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'fixModal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>🔧 Fix Namespace</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="fixModalBody"></div>
</div>
`;
document.body.appendChild(modal);
// Add close functionality
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
}
// Create content
let content = `
<div class="fix-details">
<h3>🔧 Fix Namespace: ${namespaceName}</h3>
<div class="fix-info">
<p><strong>Status:</strong> Coming Soon in Phase 2</p>
<p>This functionality will include:</p>
<ul>
<li>Auto-generating resource recommendations</li>
<li>Creating YAML patches</li>
<li>Applying fixes via OpenShift API</li>
<li>Bulk fixes for multiple namespaces</li>
</ul>
<div class="fix-actions">
<button class="btn btn-primary" onclick="document.getElementById('fixModal').style.display='none'">Close</button>
</div>
</div>
</div>
`;
// Populate and show modal
document.getElementById('fixModalBody').innerHTML = content;
modal.style.display = 'block';
}
// Apply filters
function applyFilters() {
if (!currentData) return;
// Filter namespaces based on current filters
let filteredNamespaces = currentData.namespaces || [];
if (!currentFilters.includeSystem) {
filteredNamespaces = filteredNamespaces.filter(ns =>
!ns.name.startsWith('kube-') &&
!ns.name.startsWith('openshift-') &&
!ns.name.startsWith('default')
);
}
if (currentFilters.severity !== 'all') {
filteredNamespaces = filteredNamespaces.filter(ns => {
const severity = getHighestSeverity(ns);
return severity === currentFilters.severity;
});
}
// Update table with filtered data
const filteredData = { ...currentData, namespaces: filteredNamespaces };
updateProblemTable(filteredData);
}
// Load smart recommendations
async function loadSmartRecommendations() {
try {
const response = await fetch('/api/recommendations');
const data = await response.json();
updateSmartRecommendations(data);
} catch (error) {
console.error('Error loading recommendations:', error);
document.getElementById('smartRecommendationsContent').innerHTML =
'<p style="color: #e74c3c;">Failed to load recommendations</p>';
}
}
// Update smart recommendations
function updateSmartRecommendations(data) {
const content = document.getElementById('smartRecommendationsContent');
content.innerHTML = '<p>Smart recommendations feature coming soon...</p>';
}
// Load VPA management
async function loadVPAManagement() {
try {
const response = await fetch('/api/vpa');
const data = await response.json();
updateVPAManagement(data);
} catch (error) {
console.error('Error loading VPA data:', error);
document.getElementById('vpaManagementContent').innerHTML =
'<p style="color: #e74c3c;">Failed to load VPA data</p>';
}
}
// Update VPA management
function updateVPAManagement(data) {
const content = document.getElementById('vpaManagementContent');
content.innerHTML = '<p>VPA management feature coming soon...</p>';
}
// Load historical analysis
async function loadHistoricalAnalysis() {
try {
const response = await fetch('/api/historical');
const data = await response.json();
updateHistoricalAnalysis(data);
} catch (error) {
console.error('Error loading historical data:', error);
document.getElementById('historicalAnalysisContent').innerHTML =
'<p style="color: #e74c3c;">Failed to load historical data</p>';
}
}
// Update historical analysis
function updateHistoricalAnalysis(data) {
const content = document.getElementById('historicalAnalysisContent');
content.innerHTML = '<p>Historical analysis feature coming soon...</p>';
}
// Action functions - these are defined above in the main functions
function showHistoricalAnalysis() {
// Create modal for historical analysis with real Prometheus data
let modal = document.getElementById('historicalModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'historicalModal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content" style="width: 90%; max-width: 1000px;">
<div class="modal-header">
<h2>📊 Resource Consumption Analysis - Real Numbers</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="historicalModalBody">
<div class="workload-selector">
<h3>Select Workload to Analyze:</h3>
<select id="workloadSelect" onchange="loadWorkloadMetrics()">
<option value="">Choose a workload...</option>
</select>
<select id="timeRangeSelect" onchange="loadWorkloadMetrics()">
<option value="1h">Last 1 hour</option>
<option value="6h">Last 6 hours</option>
<option value="24h" selected>Last 24 hours</option>
<option value="7d">Last 7 days</option>
</select>
</div>
<div id="metricsData" style="display: none;">
<div class="cluster-info">
<h3>🏢 Cluster Total Resources</h3>
<div id="clusterTotal"></div>
</div>
<div class="workload-metrics">
<h3>📈 Workload Resource Consumption</h3>
<div id="workloadData"></div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Add close functionality
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
}
// Populate workload selector
populateWorkloadSelector();
modal.style.display = 'block';
}
function populateWorkloadSelector() {
if (!currentData || !currentData.namespaces) return;
const select = document.getElementById('workloadSelect');
select.innerHTML = '<option value="">Choose a workload...</option>';
currentData.namespaces.forEach(namespace => {
Object.values(namespace.pods || {}).forEach(pod => {
// Extract workload name from pod name (remove random suffix)
const workloadName = pod.pod_name.replace(/-\w{10}-\w{5}$/, '');
const option = document.createElement('option');
option.value = `${namespace.namespace}/${workloadName}`;
option.textContent = `${namespace.namespace}/${workloadName}`;
select.appendChild(option);
});
});
}
async function loadWorkloadMetrics() {
const workloadSelect = document.getElementById('workloadSelect');
const timeRangeSelect = document.getElementById('timeRangeSelect');
const metricsDiv = document.getElementById('metricsData');
if (!workloadSelect.value) {
metricsDiv.style.display = 'none';
return;
}
const [namespace, workload] = workloadSelect.value.split('/');
const timeRange = timeRangeSelect.value;
try {
metricsDiv.style.display = 'block';
// Show loading message in clusterTotal div
const clusterTotalDiv = document.getElementById('clusterTotal');
const workloadDataDiv = document.getElementById('workloadData');
if (clusterTotalDiv) clusterTotalDiv.innerHTML = '<p>Loading metrics from Prometheus...</p>';
if (workloadDataDiv) workloadDataDiv.innerHTML = '';
const response = await fetch(`/api/v1/workloads/${namespace}/${workload}/metrics?time_range=${timeRange}`);
const data = await response.json();
if (data.workload_metrics) {
renderMetricsData(data);
} else {
if (clusterTotalDiv) clusterTotalDiv.innerHTML = '<p>No metrics data available for this workload.</p>';
if (workloadDataDiv) workloadDataDiv.innerHTML = '';
}
} catch (error) {
console.error('Error loading metrics:', error);
const clusterTotalDiv = document.getElementById('clusterTotal');
const workloadDataDiv = document.getElementById('workloadData');
if (clusterTotalDiv) clusterTotalDiv.innerHTML = '<p>Error loading metrics. Please try again.</p>';
if (workloadDataDiv) workloadDataDiv.innerHTML = '';
}
}
function renderMetricsData(data) {
const clusterTotalDiv = document.getElementById('clusterTotal');
const workloadDataDiv = document.getElementById('workloadData');
// Add data source indicator
const dataSourceIndicator = data.data_source === 'prometheus' ?
'<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; padding: 10px; margin-bottom: 15px; color: #155724;"><strong>✅ Live Data:</strong> Real metrics from Prometheus</div>' :
'<div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 15px; color: #721c24;"><strong>⚠️ No Data:</strong> No metrics available for this workload</div>';
// Render cluster total resources
clusterTotalDiv.innerHTML = dataSourceIndicator + `
<div class="cluster-stats">
<div class="stat-card">
<h4>CPU Total</h4>
<div class="stat-value">${data.cluster_total.cpu_cores.toFixed(2)} cores</div>
</div>
<div class="stat-card">
<h4>Memory Total</h4>
<div class="stat-value">${data.cluster_total.memory_gb.toFixed(2)} GB</div>
</div>
</div>
`;
// Render workload metrics
const cpu = data.workload_metrics.cpu;
const memory = data.workload_metrics.memory;
workloadDataDiv.innerHTML = `
<div class="metrics-grid">
<div class="metric-section">
<h4>🖥️ CPU Resources</h4>
<div class="metric-row">
<span class="metric-label">Current Usage:</span>
<span class="metric-value">${cpu.usage_cores.toFixed(3)} cores</span>
<span class="metric-percent">(${cpu.usage_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Requests:</span>
<span class="metric-value">${cpu.requests_cores.toFixed(3)} cores</span>
<span class="metric-percent">(${cpu.requests_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Limits:</span>
<span class="metric-value">${cpu.limits_cores.toFixed(3)} cores</span>
<span class="metric-percent">(${cpu.limits_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Efficiency:</span>
<span class="metric-value ${cpu.usage_cores > 0 && cpu.requests_cores > 0 ? (cpu.usage_cores / cpu.requests_cores * 100).toFixed(1) + '%' : 'N/A'}">${cpu.usage_cores > 0 && cpu.requests_cores > 0 ? (cpu.usage_cores / cpu.requests_cores * 100).toFixed(1) + '%' : 'N/A'}</span>
<span class="metric-percent">(usage vs requests)</span>
</div>
</div>
<div class="metric-section">
<h4>💾 Memory Resources</h4>
<div class="metric-row">
<span class="metric-label">Current Usage:</span>
<span class="metric-value">${memory.usage_mb.toFixed(2)} MB</span>
<span class="metric-percent">(${memory.usage_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Requests:</span>
<span class="metric-value">${memory.requests_mb.toFixed(2)} MB</span>
<span class="metric-percent">(${memory.requests_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Limits:</span>
<span class="metric-value">${memory.limits_mb.toFixed(2)} MB</span>
<span class="metric-percent">(${memory.limits_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Efficiency:</span>
<span class="metric-value ${memory.usage_bytes > 0 && memory.requests_bytes > 0 ? (memory.usage_bytes / memory.requests_bytes * 100).toFixed(1) + '%' : 'N/A'}">${memory.usage_bytes > 0 && memory.requests_bytes > 0 ? (memory.usage_bytes / memory.requests_bytes * 100).toFixed(1) + '%' : 'N/A'}</span>
<span class="metric-percent">(usage vs requests)</span>
</div>
</div>
</div>
`;
}
function exportComplianceReport() {
alert('Exporting compliance report...');
}
function fixAllIssues() {
if (confirm('Are you sure you want to fix all issues? This action cannot be undone.')) {
alert('Fixing all issues...');
}
}
function exportData() {
if (!currentData) {
alert('No data to export');
return;
}
const dataStr = JSON.stringify(currentData, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'resource-governance-report.json';
link.click();
URL.revokeObjectURL(url);
}
// Utility functions
function showLoading() {
document.getElementById('problemTableBody').innerHTML =
'<tr><td colspan="5" class="loading">Loading data...</td></tr>';
}
function showError(message) {
document.getElementById('problemTableBody').innerHTML =
`<tr><td colspan="5" style="text-align: center; color: #e74c3c;">${message}</td></tr>`;
}
function showOvercommitDetails(type) {
if (!window.overcommitData) {
alert('Overcommit data not available');
return;
}
const data = window.overcommitData;
let title, content;
if (type === 'cpu') {
title = '🖥️ CPU Overcommit Details';
const cpuCapacity = data.cpu_capacity || 0;
const cpuRequests = data.cpu_requests || 0;
content = `
<div class="overcommit-details">
<h3>CPU Resource Analysis</h3>
<div class="metric-detail">
<strong>Capacity Total:</strong> ${cpuCapacity} cores
</div>
<div class="metric-detail">
<strong>Requests Total:</strong> ${cpuRequests} cores
</div>
<div class="metric-detail">
<strong>Overcommit:</strong> ${data.cpu_overcommit_percent}% (${cpuRequests} ÷ ${cpuCapacity} × 100)
</div>
<div class="metric-detail">
<strong>Available:</strong> ${(cpuCapacity - cpuRequests).toFixed(2)} cores
</div>
</div>
`;
} else if (type === 'memory') {
title = '💾 Memory Overcommit Details';
const memoryCapacity = data.memory_capacity || 0;
const memoryRequests = data.memory_requests || 0;
const memoryCapacityGB = (memoryCapacity / (1024**3)).toFixed(1);
const memoryRequestsGB = (memoryRequests / (1024**3)).toFixed(1);
content = `
<div class="overcommit-details">
<h3>Memory Resource Analysis</h3>
<div class="metric-detail">
<strong>Capacity Total:</strong> ${memoryCapacity.toLocaleString()} bytes (≈ ${memoryCapacityGB} GB)
</div>
<div class="metric-detail">
<strong>Requests Total:</strong> ${memoryRequests.toLocaleString()} bytes (≈ ${memoryRequestsGB} GB)
</div>
<div class="metric-detail">
<strong>Overcommit:</strong> ${data.memory_overcommit_percent}% (${memoryRequestsGB} ÷ ${memoryCapacityGB} × 100)
</div>
<div class="metric-detail">
<strong>Available:</strong> ${((memoryCapacity - memoryRequests) / (1024**3)).toFixed(1)} GB
</div>
</div>
`;
}
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>${title}</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
${content}
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
// Add close functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
function showResourceUtilizationDetails() {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>📊 Resource Utilization Details</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="overcommit-details">
<h3>Resource Utilization Analysis</h3>
<div class="metric-detail">
<strong>Current Status:</strong> Placeholder Implementation
</div>
<div class="metric-detail">
<strong>Purpose:</strong> Shows actual resource usage vs. requested resources across the cluster
</div>
<div class="metric-detail">
<strong>Formula:</strong> (Total Usage ÷ Total Requests) × 100
</div>
<div class="metric-detail">
<strong>Current Value:</strong> 75% (simulated data)
</div>
<div class="metric-detail">
<strong>Implementation Status:</strong>
<span style="color: #f39c12;">⚠️ Phase 2 - Smart Recommendations Engine</span>
</div>
<div class="metric-detail">
<strong>Next Steps:</strong>
<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">
<li>Integrate real Prometheus usage metrics</li>
<li>Calculate actual cluster-wide resource utilization</li>
<li>Provide insights on resource efficiency</li>
<li>Identify over/under-provisioned workloads</li>
</ul>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
// Add close functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
function closeModal() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => modal.remove());
}
</script>
</body>
</html>