Files
openshift-resource-governance/app/static/index.html

1684 lines
60 KiB
HTML

<!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;
}
/* 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</div>
</div>
<div class="metric-card">
<div class="metric-value" id="memoryOvercommit">-</div>
<div class="metric-label">Memory Overcommit</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="resourceQuotaCoverage">-</div>
<div class="metric-label">Resource Quota Coverage</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
document.getElementById('cpuOvercommit').textContent = data.cpu_overcommit ? `${data.cpu_overcommit}%` : '0%';
document.getElementById('memoryOvercommit').textContent = data.memory_overcommit ? `${data.memory_overcommit}%` : '0%';
document.getElementById('namespacesInOvercommit').textContent = data.namespaces_in_overcommit || 0;
document.getElementById('resourceQuotaCoverage').textContent = data.resource_quota_coverage ? `${data.resource_quota_coverage}%` : '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);
}
// Create detailed HTML for namespace issues
function createNamespaceDetails(namespace) {
let html = `
<div class="namespace-details">
<h3>📋 ${namespace.namespace} - 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 => {
html += `
<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;
html += `
<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>
`;
});
html += `
</div>
<div class="validations-detail">
<h6>Issues Found:</h6>
`;
pod.validations.forEach(validation => {
const severityClass = `severity-${validation.severity}`;
html += `
<div class="validation-item ${severityClass}">
<p><strong>${validation.rule_name}:</strong> ${validation.message}</p>
<p><em>Recommendation:</em> ${validation.recommendation}</p>
</div>
`;
});
html += `
</div>
</div>
`;
});
html += `
</div>
</div>
`;
return html;
}
// 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.rule_name}:</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 === 'simulated' ?
'<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 10px; margin-bottom: 15px; color: #856404;"><strong>📊 Demo Mode:</strong> Showing simulated data for demonstration. Prometheus integration requires proper RBAC configuration.</div>' :
'<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>';
// 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.usage_cores / cpu.requests_cores * 100).toFixed(1) + '%' : 'N/A'}">${cpu.usage_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.usage_bytes / memory.requests_bytes * 100).toFixed(1) + '%' : 'N/A'}">${memory.usage_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>`;
}
</script>
</body>
</html>