- Add overcommit data processing in /cluster/status endpoint - Extract CPU/Memory capacity and requests from Prometheus - Calculate overcommit percentages and resource quota coverage - Update frontend to use new overcommit data structure - Fix issue where Cluster Overcommit Summary was showing all zeros
1691 lines
60 KiB
HTML
1691 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
|
|
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;
|
|
document.getElementById('resourceQuotaCoverage').textContent = `${data.overcommit.resource_quota_coverage}%`;
|
|
} else {
|
|
document.getElementById('cpuOvercommit').textContent = '0%';
|
|
document.getElementById('memoryOvercommit').textContent = '0%';
|
|
document.getElementById('namespacesInOvercommit').textContent = '0';
|
|
document.getElementById('resourceQuotaCoverage').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);
|
|
}
|
|
|
|
// 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">×</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">×</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">×</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>`;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|