Files
openshift-resource-governance/app/static/index.html
andersonid c6f69f85c9 fix: correct historical analysis endpoint and Chart.js loading
- Fix endpoint to use get_all_pods() instead of non-existent get_pods_by_selector()
- Move Chart.js scripts to end of body for proper loading order
- Add proper error handling for workload not found cases
- Ensure Chart.js is available before creating graphs
2025-10-02 15:47:13 -03:00

1674 lines
68 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UWRU Scanner - User Workloads and Resource Usage Scanner</title>
<!-- Red Hat Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@300;400;500;600;700&family=Red+Hat+Text:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- PatternFly 6.3.1 CSS -->
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@6.3.1/patternfly.css">
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@6.3.1/patternfly-addons.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom OpenShift-like styles -->
<style>
:root {
/* OpenShift Color Palette */
--pf-global--primary-color--100: #0066CC;
--pf-global--primary-color--200: #004080;
--pf-global--success-color--100: #3E8635;
--pf-global--warning-color--100: #F0AB00;
--pf-global--danger-color--100: #C9190B;
--pf-global--info-color--100: #009596;
/* Dark Theme Colors */
--pf-global--BackgroundColor--100: #151515;
--pf-global--BackgroundColor--200: #1E1E1E;
--pf-global--BackgroundColor--300: #2B2B2B;
--pf-global--Color--100: #FFFFFF;
--pf-global--Color--200: #F0F0F0;
--pf-global--Color--300: #D2D2D2;
--pf-global--Color--400: #8A8A8A;
/* Typography */
--pf-global--FontFamily--sans-serif: 'Red Hat Text', 'Helvetica Neue', Arial, sans-serif;
--pf-global--FontFamily--heading: 'Red Hat Display', 'Helvetica Neue', Arial, sans-serif;
}
/* Dark Mode Base */
body {
background-color: var(--pf-global--BackgroundColor--100);
color: var(--pf-global--Color--100);
font-family: var(--pf-global--FontFamily--sans-serif);
margin: 0;
padding: 0;
}
/* OpenShift-like Header */
.openshift-header {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border-bottom: 1px solid #404040;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.openshift-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.openshift-header-right {
display: flex;
align-items: center;
gap: 16px;
}
.hamburger-menu {
color: var(--pf-global--Color--100);
font-size: 18px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.hamburger-menu:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.openshift-logo {
color: var(--pf-global--Color--100);
font-family: var(--pf-global--FontFamily--heading);
font-size: 18px;
font-weight: 600;
text-decoration: none;
}
.header-icon {
color: var(--pf-global--Color--100);
font-size: 16px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s;
position: relative;
}
.header-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.notification-badge {
position: absolute;
top: 4px;
right: 4px;
background-color: var(--pf-global--danger-color--100);
color: white;
font-size: 10px;
font-weight: 600;
padding: 2px 6px;
border-radius: 10px;
min-width: 16px;
text-align: center;
}
.user-dropdown {
display: flex;
align-items: center;
gap: 8px;
color: var(--pf-global--Color--100);
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.2s;
}
.user-dropdown:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* OpenShift-like Sidebar */
.openshift-sidebar {
background-color: #000000;
width: 280px;
height: calc(100vh - 60px);
position: fixed;
left: 0;
top: 60px;
overflow-y: auto;
border-right: 1px solid #404040;
z-index: 1000;
}
.sidebar-section {
padding: 16px 0;
border-bottom: 1px solid #404040;
}
.sidebar-section-title {
color: var(--pf-global--Color--400);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0 24px 8px;
margin: 0;
}
.sidebar-nav {
list-style: none;
margin: 0;
padding: 0;
}
.sidebar-nav-item {
margin: 0;
}
.sidebar-nav-link {
display: flex;
align-items: center;
gap: 12px;
color: var(--pf-global--Color--100);
text-decoration: none;
padding: 12px 24px;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.sidebar-nav-link:hover {
background-color: rgba(255, 255, 255, 0.05);
color: var(--pf-global--Color--100);
}
.sidebar-nav-link.active {
background-color: rgba(0, 102, 204, 0.1);
border-left-color: var(--pf-global--primary-color--100);
color: var(--pf-global--primary-color--100);
}
.sidebar-nav-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.sidebar-nav-arrow {
margin-left: auto;
font-size: 12px;
opacity: 0.6;
}
/* Main Content Area */
.main-content {
margin-left: 280px;
padding: 24px;
background-color: var(--pf-global--BackgroundColor--100);
min-height: calc(100vh - 60px);
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-family: var(--pf-global--FontFamily--heading);
font-size: 28px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0 0 8px 0;
}
.page-description {
color: var(--pf-global--Color--300);
font-size: 16px;
margin: 0;
}
/* OpenShift-like Cards */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 24px;
}
.openshift-card {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
padding: 24px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.openshift-card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.card-title {
font-family: var(--pf-global--FontFamily--heading);
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.card-action {
color: var(--pf-global--primary-color--100);
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.card-action:hover {
text-decoration: underline;
}
/* Metrics Cards */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.metric-card {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.metric-value {
font-family: var(--pf-global--FontFamily--heading);
font-size: 32px;
font-weight: 700;
color: var(--pf-global--Color--100);
margin: 0 0 8px 0;
}
.metric-label {
color: var(--pf-global--Color--300);
font-size: 14px;
font-weight: 500;
margin: 0;
}
.metric-icon {
font-size: 24px;
margin-bottom: 12px;
}
.metric-icon.success {
color: var(--pf-global--success-color--100);
}
.metric-icon.warning {
color: var(--pf-global--warning-color--100);
}
.metric-icon.danger {
color: var(--pf-global--danger-color--100);
}
.metric-icon.info {
color: var(--pf-global--info-color--100);
}
/* Status Indicators */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-indicator.success {
background-color: rgba(62, 134, 53, 0.2);
color: var(--pf-global--success-color--100);
border: 1px solid rgba(62, 134, 53, 0.3);
}
.status-indicator.warning {
background-color: rgba(240, 171, 0, 0.2);
color: var(--pf-global--warning-color--100);
border: 1px solid rgba(240, 171, 0, 0.3);
}
.status-indicator.danger {
background-color: rgba(201, 25, 11, 0.2);
color: var(--pf-global--danger-color--100);
border: 1px solid rgba(201, 25, 11, 0.3);
}
.status-indicator.info {
background-color: rgba(0, 149, 150, 0.2);
color: var(--pf-global--info-color--100);
border: 1px solid rgba(0, 149, 150, 0.3);
}
/* Tables */
.openshift-table {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.table-header {
background: linear-gradient(135deg, #404040 0%, #2B2B2B 100%);
padding: 16px 24px;
border-bottom: 1px solid #404040;
}
.table-title {
font-family: var(--pf-global--FontFamily--heading);
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.table-content {
padding: 0;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
background-color: #404040;
color: var(--pf-global--Color--100);
font-weight: 600;
font-size: 14px;
padding: 12px 24px;
text-align: left;
border-bottom: 1px solid #404040;
}
.table td {
color: var(--pf-global--Color--200);
font-size: 14px;
padding: 12px 24px;
border-bottom: 1px solid #404040;
}
.table tr:hover {
background-color: rgba(255, 255, 255, 0.02);
}
/* Buttons */
.openshift-button {
background: linear-gradient(135deg, var(--pf-global--primary-color--100) 0%, var(--pf-global--primary-color--200) 100%);
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.openshift-button:hover {
background: linear-gradient(135deg, var(--pf-global--primary-color--200) 0%, #003366 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 102, 204, 0.3);
}
.openshift-button.secondary {
background: linear-gradient(135deg, #404040 0%, #2B2B2B 100%);
color: var(--pf-global--Color--100);
}
.openshift-button.secondary:hover {
background: linear-gradient(135deg, #505050 0%, #404040 100%);
}
/* Loading States */
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: var(--pf-global--Color--300);
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #404040;
border-top: 2px solid var(--pf-global--primary-color--100);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.openshift-sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.openshift-sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Hidden sections */
.section-hidden {
display: none !important;
}
/* Accordion Styles */
.expand-button {
background: none;
border: none;
color: var(--pf-global--Color--100);
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.expand-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.workload-details-row {
background-color: #1A1A1A;
}
.workload-details-container {
padding: 0;
}
.workload-row:hover {
background-color: rgba(255, 255, 255, 0.02);
}
/* 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.8);
overflow-y: auto;
}
.modal-content {
background-color: var(--pf-global--BackgroundColor--100);
margin: 2% auto;
padding: 0;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
color: var(--pf-global--Color--100);
padding: 20px 24px;
border-bottom: 1px solid #404040;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px 8px 0 0;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.close {
color: var(--pf-global--Color--300);
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s ease;
}
.close:hover {
color: var(--pf-global--Color--100);
}
.modal-body {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
.status-indicator {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-indicator.danger {
background-color: var(--pf-global--danger-color--100);
color: white;
}
.status-indicator.warning {
background-color: var(--pf-global--warning-color--100);
color: black;
}
.status-indicator.info {
background-color: var(--pf-global--info-color--100);
color: white;
}
.status-indicator.success {
background-color: var(--pf-global--success-color--100);
color: white;
}
</style>
</head>
<body>
<!-- OpenShift-like Header -->
<header class="openshift-header">
<div class="openshift-header-left">
<a href="#" class="openshift-logo">UWRU Scanner</a>
</div>
<div class="openshift-header-right">
<div class="header-icon">
<i class="fas fa-th"></i>
</div>
<div class="header-icon" style="position: relative;">
<i class="fas fa-bell"></i>
<span class="notification-badge">46</span>
</div>
<div class="header-icon">
<i class="fas fa-plus"></i>
</div>
<div class="header-icon">
<i class="fas fa-question-circle"></i>
</div>
<div class="user-dropdown">
<i class="fas fa-user-circle"></i>
<span>anobre</span>
<i class="fas fa-chevron-down"></i>
</div>
</div>
</header>
<!-- OpenShift-like Sidebar -->
<nav class="openshift-sidebar" id="sidebar">
<div class="sidebar-section">
<div class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link active" data-section="workload-scanner">
<i class="fas fa-home sidebar-nav-icon"></i>
<span>Home</span>
</a>
</li>
</div>
</div>
<div class="sidebar-section">
<h3 class="sidebar-section-title">Favorites</h3>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="workload-scanner">
<i class="fas fa-search sidebar-nav-icon"></i>
<span>UWRU Scanner</span>
</a>
</li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="historical-analysis">
<i class="fas fa-chart-line sidebar-nav-icon"></i>
<span>Historical Analysis</span>
</a>
</li>
</ul>
</div>
<div class="sidebar-section">
<h3 class="sidebar-section-title">Workloads</h3>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link">
<i class="fas fa-cube sidebar-nav-icon"></i>
<span>Pods</span>
<i class="fas fa-chevron-right sidebar-nav-arrow"></i>
</a>
</li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link">
<i class="fas fa-layer-group sidebar-nav-icon"></i>
<span>Deployments</span>
<i class="fas fa-chevron-right sidebar-nav-arrow"></i>
</a>
</li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link">
<i class="fas fa-server sidebar-nav-icon"></i>
<span>Services</span>
<i class="fas fa-chevron-right sidebar-nav-arrow"></i>
</a>
</li>
</ul>
</div>
<div class="sidebar-section">
<h3 class="sidebar-section-title">Observe</h3>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link">
<i class="fas fa-chart-bar sidebar-nav-icon"></i>
<span>Metrics</span>
<i class="fas fa-chevron-right sidebar-nav-arrow"></i>
</a>
</li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link">
<i class="fas fa-search sidebar-nav-icon"></i>
<span>Logs</span>
<i class="fas fa-chevron-right sidebar-nav-arrow"></i>
</a>
</li>
</ul>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Workload Scanner Section -->
<section id="workload-scanner-section">
<div class="page-header">
<h1 class="page-title">UWRU Scanner</h1>
<p class="page-description">User Workloads and Resource Usage Scanner - Identify and analyze workloads with resource configuration issues</p>
</div>
<!-- Metrics Cards -->
<div class="metrics-grid" id="metrics-grid">
<div class="metric-card">
<div class="metric-icon success">
<i class="fas fa-cube"></i>
</div>
<div class="metric-value" id="total-workloads">-</div>
<div class="metric-label">Total Workloads</div>
</div>
<div class="metric-card">
<div class="metric-icon info">
<i class="fas fa-layer-group"></i>
</div>
<div class="metric-value" id="total-namespaces">-</div>
<div class="metric-label">Namespaces</div>
</div>
<div class="metric-card">
<div class="metric-icon danger">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="metric-value" id="critical-issues">-</div>
<div class="metric-label">Critical Issues</div>
</div>
<div class="metric-card">
<div class="metric-icon warning">
<i class="fas fa-exclamation-circle"></i>
</div>
<div class="metric-value" id="total-warnings">-</div>
<div class="metric-label">Warnings</div>
</div>
</div>
<!-- Workloads Table Card -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Workloads with Issues</h2>
<button class="openshift-button" id="refresh-workloads">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<div class="table-content" id="workloads-table-container">
<div class="loading-spinner">
<div class="spinner"></div>
Loading workloads...
</div>
</div>
</div>
</section>
<!-- Historical Analysis Section -->
<section id="historical-analysis-section" class="section-hidden">
<div class="page-header">
<h1 class="page-title">Historical Analysis</h1>
<p class="page-description">Resource consumption analysis and historical data</p>
</div>
<!-- Workloads List Card -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Available Workloads</h2>
<button class="openshift-button" id="refresh-historical">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<div class="table-content" id="historical-workloads-container">
<div class="loading-spinner">
<div class="spinner"></div>
Loading historical data...
</div>
</div>
</div>
<!-- Workload Details Card (hidden initially) -->
<div class="openshift-card section-hidden" id="workload-details-container">
<div class="card-header">
<h2 class="card-title" id="workload-details-title">Workload Details</h2>
<button class="openshift-button secondary" id="close-workload-details">
<i class="fas fa-times"></i>
Close
</button>
</div>
<div class="table-content" id="workload-details-content">
<!-- Workload details will be populated here -->
</div>
</div>
</section>
</main>
<!-- JavaScript -->
<script>
// Global variables
let currentData = null;
let currentSection = 'workload-scanner';
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
function initializeApp() {
// Setup navigation
setupNavigation();
// Load initial data
loadWorkloadScanner();
}
function setupNavigation() {
// Sidebar navigation
const navLinks = document.querySelectorAll('.sidebar-nav-link[data-section]');
navLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const section = this.getAttribute('data-section');
showSection(section);
});
});
// Close workload details
document.getElementById('close-workload-details').addEventListener('click', function() {
document.getElementById('workload-details-container').classList.add('section-hidden');
});
// Refresh buttons
document.getElementById('refresh-workloads').addEventListener('click', loadWorkloadScanner);
document.getElementById('refresh-historical').addEventListener('click', loadHistoricalAnalysis);
}
function showSection(section) {
// Hide all sections
document.querySelectorAll('main section').forEach(sec => {
sec.classList.add('section-hidden');
});
// Show selected section
document.getElementById(section + '-section').classList.remove('section-hidden');
// Update active nav item
document.querySelectorAll('.sidebar-nav-link').forEach(link => {
link.classList.remove('active');
});
document.querySelector(`.sidebar-nav-link[data-section="${section}"]`).classList.add('active');
currentSection = section;
// Load section data
if (section === 'workload-scanner') {
loadWorkloadScanner();
} else if (section === 'historical-analysis') {
loadHistoricalAnalysis();
}
}
async function loadWorkloadScanner() {
try {
showLoading('workloads-table-container');
// Load cluster status
const clusterResponse = await fetch('/api/v1/cluster/status');
const clusterData = await clusterResponse.json();
// Load validations
const validationsResponse = await fetch('/api/v1/validations');
const validationsData = await validationsResponse.json();
currentData = { cluster: clusterData, validations: validationsData };
// Update metrics cards
updateMetricsCards(clusterData);
// Update workloads table
updateWorkloadsTable(validationsData);
} catch (error) {
console.error('Error loading workload scanner data:', error);
showError('workloads-table-container', 'Failed to load workload data');
}
}
async function loadHistoricalAnalysis() {
try {
showLoading('historical-workloads-container');
// Load historical data
const response = await fetch('/api/v1/historical-analysis');
const data = await response.json();
updateHistoricalWorkloads(data);
} catch (error) {
console.error('Error loading historical analysis data:', error);
showError('historical-workloads-container', 'Failed to load historical data');
}
}
function updateMetricsCards(data) {
document.getElementById('total-workloads').textContent = data.total_pods || 0;
document.getElementById('total-namespaces').textContent = data.total_namespaces || 0;
document.getElementById('critical-issues').textContent = data.critical_issues || 0;
document.getElementById('total-warnings').textContent = data.total_warnings || 0;
}
function updateWorkloadsTable(data) {
const container = document.getElementById('workloads-table-container');
// Group validations by namespace
const namespaceGroups = {};
if (data.validations && data.validations.length > 0) {
data.validations.forEach(validation => {
const namespace = validation.namespace;
if (!namespaceGroups[namespace]) {
namespaceGroups[namespace] = {
namespace: namespace,
validations: [],
pods: new Set(),
severity_breakdown: { error: 0, warning: 0, info: 0 }
};
}
namespaceGroups[namespace].validations.push(validation);
namespaceGroups[namespace].pods.add(validation.pod_name);
// Count severity
if (validation.severity === 'error') {
namespaceGroups[namespace].severity_breakdown.error++;
} else if (validation.severity === 'warning') {
namespaceGroups[namespace].severity_breakdown.warning++;
} else if (validation.severity === 'info') {
namespaceGroups[namespace].severity_breakdown.info++;
}
});
}
const namespaces = Object.values(namespaceGroups);
if (namespaces.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--success-color--100);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Issues Found</h3>
<p style="margin: 0;">All workloads are properly configured</p>
</div>
`;
return;
}
const tableHTML = `
<table class="table">
<thead>
<tr>
<th>Namespace</th>
<th>Pods</th>
<th>Issues</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${namespaces.map(namespace => `
<tr>
<td>
<strong style="color: var(--pf-global--Color--100);">${namespace.namespace}</strong>
</td>
<td>${namespace.pods.size}</td>
<td>${namespace.validations.length}</td>
<td>
<span class="status-indicator ${getSeverityClass(namespace)}">
${getSeverityText(namespace)}
</span>
</td>
<td>
<button class="openshift-button secondary" onclick="analyzeNamespace('${namespace.namespace}')">
<i class="fas fa-search"></i>
Analyze
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
function updateHistoricalWorkloads(data) {
const container = document.getElementById('historical-workloads-container');
if (!data.workloads || data.workloads.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-chart-line" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--info-color--100);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Historical Data</h3>
<p style="margin: 0;">No workloads available for analysis</p>
</div>
`;
return;
}
const tableHTML = `
<table class="table">
<thead>
<tr>
<th style="width: 30px;"></th>
<th>Workload</th>
<th>Namespace</th>
<th>Pods</th>
<th>CPU Usage</th>
<th>Memory Usage</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${data.workloads.map((workload, index) => `
<tr class="workload-row" data-workload="${workload.name}" data-namespace="${workload.namespace}">
<td>
<button class="expand-button" onclick="toggleWorkloadDetails(${index})" id="expand-btn-${index}">
<i class="fas fa-chevron-right"></i>
</button>
</td>
<td>
<strong style="color: var(--pf-global--Color--100);">${workload.name}</strong>
</td>
<td>${workload.namespace}</td>
<td>${workload.pod_count || 0}</td>
<td>${workload.cpu_usage || 'N/A'}</td>
<td>${workload.memory_usage || 'N/A'}</td>
<td>${workload.last_updated ? new Date(workload.last_updated).toLocaleString() : 'N/A'}</td>
<td>
<button class="openshift-button" onclick="loadWorkloadDetails('${workload.name}', '${workload.namespace}', ${index})">
<i class="fas fa-chart-line"></i>
Load Details
</button>
</td>
</tr>
<tr class="workload-details-row" id="details-${index}" style="display: none;">
<td colspan="8">
<div class="workload-details-container" id="details-content-${index}">
<div class="loading-spinner">
<div class="spinner"></div>
Loading workload details...
</div>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
function showWorkloadDetails(workloadName, namespace) {
// Update title
document.getElementById('workload-details-title').textContent = `${workloadName} - ${namespace}`;
// Load workload details
loadWorkloadDetails(workloadName, namespace);
// Show details container
document.getElementById('workload-details-container').classList.remove('section-hidden');
}
async function loadWorkloadDetails(workloadName, namespace) {
try {
const response = await fetch(`/api/v1/historical-analysis/${namespace}/${workloadName}`);
const data = await response.json();
updateWorkloadDetails(data);
} catch (error) {
console.error('Error loading workload details:', error);
document.getElementById('workload-details-content').innerHTML =
'<div style="text-align: center; padding: 40px; color: var(--pf-global--danger-color--100);">Failed to load workload details</div>';
}
}
function toggleWorkloadDetails(index) {
const detailsRow = document.getElementById(`details-${index}`);
const expandBtn = document.getElementById(`expand-btn-${index}`);
const icon = expandBtn.querySelector('i');
if (detailsRow.style.display === 'none') {
detailsRow.style.display = 'table-row';
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-down');
} else {
detailsRow.style.display = 'none';
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-right');
}
}
async function loadWorkloadDetails(workloadName, namespace, index) {
const container = document.getElementById(`details-content-${index}`);
try {
container.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
Loading workload details...
</div>
`;
const response = await fetch(`/api/v1/historical-analysis/${namespace}/${workloadName}`);
const data = await response.json();
updateWorkloadDetailsAccordion(data, index);
} catch (error) {
console.error('Error loading workload details:', error);
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--danger-color--100);">
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px;"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">Error</h3>
<p style="margin: 0;">Failed to load workload details</p>
</div>
`;
}
}
function updateWorkloadDetailsAccordion(data, index) {
const container = document.getElementById(`details-content-${index}`);
// Create chart containers with unique IDs
const cpuChartId = `cpu-chart-${index}`;
const memoryChartId = `memory-chart-${index}`;
// Parse CPU and Memory data
const cpuData = data.cpu_data || {};
const memoryData = data.memory_data || {};
const recommendations = data.recommendations || [];
container.innerHTML = `
<div style="padding: 24px; background-color: #1E1E1E; border-radius: 8px; margin: 16px 0;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px;">
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-microchip" style="margin-right: 8px; color: var(--pf-global--info-color--100);"></i>
CPU Usage (24h)
</h3>
</div>
<div style="padding: 20px;">
<div style="height: 300px; position: relative;">
<canvas id="${cpuChartId}" width="400" height="200"></canvas>
</div>
${cpuData.data && cpuData.data.length > 0 ? `
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Current</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getCurrentValue(cpuData.data)} cores</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Average</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getAverageValue(cpuData.data)} cores</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Peak</div>
<div style="font-weight: bold; color: var(--pf-global--warning-color--100);">${getPeakValue(cpuData.data)} cores</div>
</div>
</div>
` : ''}
</div>
</div>
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-memory" style="margin-right: 8px; color: var(--pf-global--warning-color--100);"></i>
Memory Usage (24h)
</h3>
</div>
<div style="padding: 20px;">
<div style="height: 300px; position: relative;">
<canvas id="${memoryChartId}" width="400" height="200"></canvas>
</div>
${memoryData.data && memoryData.data.length > 0 ? `
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Current</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getCurrentValue(memoryData.data)} MB</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Average</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getAverageValue(memoryData.data)} MB</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Peak</div>
<div style="font-weight: bold; color: var(--pf-global--warning-color--100);">${getPeakValue(memoryData.data)} MB</div>
</div>
</div>
` : ''}
</div>
</div>
</div>
${recommendations.length > 0 ? `
<div class="openshift-card" style="margin-top: 24px;">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-lightbulb" style="margin-right: 8px; color: var(--pf-global--success-color--100);"></i>
Recommendations
</h3>
</div>
<div style="padding: 20px;">
${recommendations.map(rec => `
<div style="margin-bottom: 16px; padding: 16px; background-color: #2B2B2B; border-radius: 6px; border-left: 4px solid ${getSeverityColor(rec.severity)};">
<p style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);"><strong>${rec.type}:</strong> ${rec.message}</p>
<p style="margin: 0; color: var(--pf-global--Color--300);"><em>Recommendation:</em> ${rec.recommendation}</p>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
// Create charts after DOM is updated
setTimeout(() => {
createCPUChart(cpuChartId, cpuData);
createMemoryChart(memoryChartId, memoryData);
}, 100);
}
function getCurrentValue(data) {
if (!data || data.length === 0) return '0.000';
const lastValue = data[data.length - 1];
return lastValue ? lastValue.y.toFixed(3) : '0.000';
}
function getAverageValue(data) {
if (!data || data.length === 0) return '0.000';
const sum = data.reduce((acc, point) => acc + point.y, 0);
return (sum / data.length).toFixed(3);
}
function getPeakValue(data) {
if (!data || data.length === 0) return '0.000';
const max = Math.max(...data.map(point => point.y));
return max.toFixed(3);
}
function createCPUChart(canvasId, cpuData) {
const ctx = document.getElementById(canvasId);
if (!ctx) return;
const chartData = cpuData?.data || [];
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'CPU Usage (cores)',
data: chartData,
borderColor: '#0066CC',
backgroundColor: 'rgba(0, 102, 204, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#FFFFFF'
}
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
day: 'MMM dd'
}
},
ticks: {
color: '#FFFFFF'
},
grid: {
color: '#404040'
}
},
y: {
beginAtZero: true,
ticks: {
color: '#FFFFFF',
callback: function(value) {
return value.toFixed(3) + ' cores';
}
},
grid: {
color: '#404040'
}
}
}
}
});
}
function createMemoryChart(canvasId, memoryData) {
const ctx = document.getElementById(canvasId);
if (!ctx) return;
const chartData = memoryData?.data || [];
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Memory Usage (MB)',
data: chartData,
borderColor: '#CC0000',
backgroundColor: 'rgba(204, 0, 0, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#FFFFFF'
}
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
day: 'MMM dd'
}
},
ticks: {
color: '#FFFFFF'
},
grid: {
color: '#404040'
}
},
y: {
beginAtZero: true,
ticks: {
color: '#FFFFFF',
callback: function(value) {
return value.toFixed(1) + ' MB';
}
},
grid: {
color: '#404040'
}
}
}
}
});
}
function updateWorkloadDetails(data) {
const container = document.getElementById('workload-details-content');
container.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px;">
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">CPU Usage</h3>
</div>
<div style="padding: 20px; text-align: center; color: var(--pf-global--Color--300);">
<i class="fas fa-microchip" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--info-color--100);"></i>
<p>CPU usage data will be displayed here</p>
</div>
</div>
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">Memory Usage</h3>
</div>
<div style="padding: 20px; text-align: center; color: var(--pf-global--Color--300);">
<i class="fas fa-memory" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--warning-color--100);"></i>
<p>Memory usage data will be displayed here</p>
</div>
</div>
</div>
`;
}
function analyzeNamespace(namespaceName) {
if (!currentData || !currentData.validations || !currentData.validations.validations) return;
// Filter validations for this namespace
const namespaceValidations = currentData.validations.validations.filter(v => v.namespace === namespaceName);
if (namespaceValidations.length === 0) return;
// Group by pod
const podGroups = {};
namespaceValidations.forEach(validation => {
const podName = validation.pod_name;
if (!podGroups[podName]) {
podGroups[podName] = {
pod_name: podName,
namespace: namespaceName,
phase: 'Running', // Default phase
node_name: 'Unknown', // Default node
containers: [],
validations: []
};
}
podGroups[podName].validations.push(validation);
});
// Create namespace object for compatibility
const namespace = {
namespace: namespaceName,
pods: podGroups,
validations: namespaceValidations,
severity_breakdown: {
error: namespaceValidations.filter(v => v.severity === 'error').length,
warning: namespaceValidations.filter(v => v.severity === 'warning').length,
info: namespaceValidations.filter(v => v.severity === 'info').length
}
};
// Show details in modal
showNamespaceDetails(namespaceName, namespace);
}
function showNamespaceDetails(namespaceName, namespace) {
// 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" style="width: 90%; max-width: 1000px;">
<div class="modal-header">
<h2>📋 Namespace Analysis - ${namespaceName}</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';
};
}
// Use the passed namespace object
if (!namespace) return;
let content = `
<div class="namespace-details">
<div class="namespace-summary" style="background-color: #1E1E1E; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h3 style="color: var(--pf-global--Color--100); margin-top: 0;">📊 Summary</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<div>
<strong>Pods:</strong> ${Object.keys(namespace.pods || {}).length}
</div>
<div>
<strong>Total Issues:</strong> ${namespace.validations?.length || 0}
</div>
<div>
<strong>Errors:</strong> <span style="color: var(--pf-global--danger-color--100);">${namespace.severity_breakdown?.error || 0}</span>
</div>
<div>
<strong>Warnings:</strong> <span style="color: var(--pf-global--warning-color--100);">${namespace.severity_breakdown?.warning || 0}</span>
</div>
</div>
</div>
<div class="pods-details">
<h3 style="color: var(--pf-global--Color--100);">🔍 Pod Analysis</h3>
`;
// Add details for each pod
Object.values(namespace.pods || {}).forEach(pod => {
content += `
<div class="pod-detail" style="background-color: #1E1E1E; padding: 20px; border-radius: 8px; margin-bottom: 16px; border: 1px solid #404040;">
<h4 style="color: var(--pf-global--Color--100); margin-top: 0;">📦 ${pod.pod_name}</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px;">
<div><strong>Status:</strong> ${pod.phase}</div>
<div><strong>Node:</strong> ${pod.node_name}</div>
</div>
<div class="containers-detail">
<h5 style="color: var(--pf-global--Color--100);">Containers:</h5>
`;
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" style="background-color: #2B2B2B; padding: 16px; border-radius: 6px; margin-bottom: 12px;">
<h6 style="color: var(--pf-global--Color--100); margin-top: 0;">${container.name}</h6>
<p><strong>Image:</strong> ${container.image}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div>
<strong>Requests:</strong>
${hasRequests ?
`<span style="color: var(--pf-global--success-color--100);">${JSON.stringify(container.resources.requests)}</span>` :
'<span style="color: var(--pf-global--danger-color--100);">❌ Not defined</span>'
}
</div>
<div>
<strong>Limits:</strong>
${hasLimits ?
`<span style="color: var(--pf-global--success-color--100);">${JSON.stringify(container.resources.limits)}</span>` :
'<span style="color: var(--pf-global--warning-color--100);">❌ Not defined</span>'
}
</div>
</div>
</div>
`;
});
content += `
</div>
<div class="validations-detail">
<h5 style="color: var(--pf-global--Color--100);">Issues Found:</h5>
`;
if (pod.validations && pod.validations.length > 0) {
pod.validations.forEach(validation => {
const severityClass = validation.severity === 'error' ? 'danger' :
validation.severity === 'warning' ? 'warning' : 'info';
const severityColor = validation.severity === 'error' ? 'var(--pf-global--danger-color--100)' :
validation.severity === 'warning' ? 'var(--pf-global--warning-color--100)' : 'var(--pf-global--info-color--100)';
content += `
<div class="validation-item" style="background-color: #2B2B2B; padding: 16px; border-radius: 6px; margin-bottom: 12px; border-left: 4px solid ${severityColor};">
<p style="margin: 0 0 8px 0;"><strong style="color: ${severityColor};">${validation.rule_name}:</strong> ${validation.message}</p>
<p style="margin: 0; color: var(--pf-global--Color--300);"><em>Recommendation:</em> ${validation.recommendation}</p>
</div>
`;
});
} else {
content += `<p style="color: var(--pf-global--success-color--100);">✅ 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';
}
function getSeverityClass(namespace) {
const breakdown = namespace.severity_breakdown || {};
if (breakdown.error > 0) return 'danger';
if (breakdown.warning > 0) return 'warning';
if (breakdown.info > 0) return 'info';
return 'success';
}
function getSeverityText(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 'Healthy';
}
function showLoading(containerId) {
const container = document.getElementById(containerId);
container.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
Loading...
</div>
`;
}
function showError(containerId, message) {
const container = document.getElementById(containerId);
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--danger-color--100);">
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px;"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">Error</h3>
<p style="margin: 0;">${message}</p>
</div>
`;
}
</script>
<!-- Chart.js for historical analysis graphs -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
</body>
</html>