- Include PromQL queries in API response for workload metrics - Display queries in historical analysis modal with copy functionality - Add professional styling for query display sections - Enable users to copy and validate queries in OpenShift Console - Organize queries by category: cluster totals, usage, requests, limits - Add copy-to-clipboard functionality with visual feedback
1991 lines
72 KiB
HTML
1991 lines
72 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;
|
||
}
|
||
|
||
.info-icon {
|
||
cursor: pointer;
|
||
color: #3498db;
|
||
font-size: 14px;
|
||
margin-left: 5px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.info-icon:hover {
|
||
color: #2980b9;
|
||
}
|
||
|
||
.overcommit-details {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.overcommit-details h3 {
|
||
color: #2c3e50;
|
||
margin-bottom: 1rem;
|
||
border-bottom: 2px solid #3498db;
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
|
||
.metric-detail {
|
||
margin: 0.75rem 0;
|
||
padding: 0.5rem;
|
||
background: #f8f9fa;
|
||
border-left: 3px solid #3498db;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.metric-detail strong {
|
||
color: #2c3e50;
|
||
}
|
||
|
||
/* Status Overview */
|
||
.status-overview {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
padding: 1.5rem;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 3rem;
|
||
}
|
||
|
||
.status-content h3 {
|
||
margin-bottom: 0.5rem;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.status-content p {
|
||
color: #7f8c8d;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: #fefefe;
|
||
margin: 5% auto;
|
||
padding: 0;
|
||
border: 1px solid #888;
|
||
border-radius: 8px;
|
||
width: 80%;
|
||
max-width: 1000px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #dee2e6;
|
||
border-radius: 8px 8px 0 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
color: #495057;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.close {
|
||
color: #aaa;
|
||
float: right;
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.close:hover {
|
||
color: black;
|
||
}
|
||
|
||
/* Namespace Details Styles */
|
||
.namespace-details {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
}
|
||
|
||
.namespace-summary {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.namespace-summary ul {
|
||
margin: 10px 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.pod-detail {
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 5px;
|
||
margin: 15px 0;
|
||
padding: 15px;
|
||
background: #fff;
|
||
}
|
||
|
||
.pod-detail h5 {
|
||
color: #495057;
|
||
margin-bottom: 10px;
|
||
border-bottom: 1px solid #dee2e6;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.containers-detail {
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.container-detail {
|
||
background: #f8f9fa;
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
border-radius: 3px;
|
||
border-left: 4px solid #007bff;
|
||
}
|
||
|
||
.validations-detail {
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.validation-item {
|
||
padding: 10px;
|
||
margin: 8px 0;
|
||
border-radius: 3px;
|
||
border-left: 4px solid #6c757d;
|
||
}
|
||
|
||
.validation-item.severity-error {
|
||
background: #f8d7da;
|
||
border-left-color: #dc3545;
|
||
}
|
||
|
||
.validation-item.severity-warning {
|
||
background: #fff3cd;
|
||
border-left-color: #ffc107;
|
||
}
|
||
|
||
.validation-item.severity-info {
|
||
background: #d1ecf1;
|
||
border-left-color: #17a2b8;
|
||
}
|
||
|
||
.validation-item p {
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.validation-item strong {
|
||
color: #495057;
|
||
}
|
||
|
||
.validation-item em {
|
||
color: #6c757d;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
/* Fix Modal Styles */
|
||
.fix-details {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
}
|
||
|
||
.fix-info {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.fix-info ul {
|
||
margin: 10px 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.fix-actions {
|
||
margin-top: 20px;
|
||
text-align: right;
|
||
}
|
||
|
||
.workload-selector {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.workload-selector select {
|
||
margin: 0 10px;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.chart-container {
|
||
margin: 20px 0;
|
||
padding: 15px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
background: white;
|
||
}
|
||
|
||
.chart-container canvas {
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.cluster-stats {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.stat-card h4 {
|
||
margin: 0 0 10px 0;
|
||
color: #495057;
|
||
font-size: 14px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #007bff;
|
||
}
|
||
|
||
.metrics-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 30px;
|
||
}
|
||
|
||
.metric-section {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.metric-section h4 {
|
||
margin: 0 0 20px 0;
|
||
color: #495057;
|
||
border-bottom: 2px solid #e9ecef;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.metric-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #f8f9fa;
|
||
}
|
||
|
||
.metric-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.metric-label {
|
||
font-weight: 500;
|
||
color: #6c757d;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.metric-value {
|
||
font-weight: bold;
|
||
color: #212529;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.metric-percent {
|
||
color: #6c757d;
|
||
font-size: 12px;
|
||
font-style: italic;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.metrics-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.cluster-stats {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
/* Problem Summary Table */
|
||
.problem-summary {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.filters {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.filter-group label {
|
||
font-weight: 500;
|
||
color: #34495e;
|
||
}
|
||
|
||
.filter-group select,
|
||
.filter-group input {
|
||
padding: 0.5rem;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.filter-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.filter-checkbox input[type="checkbox"] {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.apply-filters-btn {
|
||
background: #3498db;
|
||
color: white;
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.apply-filters-btn:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
/* Table Styles */
|
||
.table-container {
|
||
overflow-x: auto;
|
||
border-radius: 8px;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: white;
|
||
}
|
||
|
||
th {
|
||
background: #f8f9fa;
|
||
padding: 1rem;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
border-bottom: 2px solid #e0e0e0;
|
||
}
|
||
|
||
td {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
/* Severity Badges */
|
||
.severity-badge {
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 12px;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.severity-critical {
|
||
background: #ffebee;
|
||
color: #c62828;
|
||
}
|
||
|
||
.severity-error {
|
||
background: #ffebee;
|
||
color: #d32f2f;
|
||
}
|
||
|
||
.severity-warning {
|
||
background: #fff3e0;
|
||
color: #f57c00;
|
||
}
|
||
|
||
.severity-info {
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
}
|
||
|
||
/* Action Buttons */
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.btn {
|
||
padding: 0.5rem 1rem;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #229954;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #e67e22;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 0.25rem 0.75rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
/* Quick Actions */
|
||
.quick-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-top: 2rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.quick-action-btn {
|
||
background: #34495e;
|
||
color: white;
|
||
border: none;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.quick-action-btn:hover {
|
||
background: #2c3e50;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
/* Loading States */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
.loading::after {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid #f3f3f3;
|
||
border-top: 2px solid #3498db;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.metrics-grid {
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
}
|
||
|
||
.filters {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.quick-actions {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
/* PromQL Queries Styles */
|
||
.promql-queries {
|
||
margin-top: 30px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.promql-queries h4 {
|
||
color: #495057;
|
||
margin-bottom: 15px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.promql-queries h5 {
|
||
color: #6c757d;
|
||
margin: 20px 0 10px 0;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.query-section {
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.query-item {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.query-item label {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
margin-bottom: 5px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.query-box {
|
||
display: flex;
|
||
align-items: center;
|
||
background: white;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.query-box code {
|
||
flex: 1;
|
||
background: none;
|
||
border: none;
|
||
padding: 0;
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
font-size: 0.85rem;
|
||
color: #e83e8c;
|
||
word-break: break-all;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.copy-btn {
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 6px 12px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.copy-btn:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.copy-btn:active {
|
||
background: #004085;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Sidebar -->
|
||
<div class="sidebar">
|
||
<div class="sidebar-header">
|
||
<h2>Resource Governance</h2>
|
||
</div>
|
||
<nav class="sidebar-nav">
|
||
<a href="#" class="nav-item active" data-section="dashboard">
|
||
<span class="nav-icon">🏠</span>
|
||
<span class="nav-text">Dashboard</span>
|
||
</a>
|
||
<div class="nav-divider"></div>
|
||
<a href="#" class="nav-item" data-section="smart-recommendations">
|
||
<span class="nav-icon">🎯</span>
|
||
<span class="nav-text">Smart Recommendations</span>
|
||
</a>
|
||
<a href="#" class="nav-item" data-section="vpa-management">
|
||
<span class="nav-icon">⚙️</span>
|
||
<span class="nav-text">VPA Management</span>
|
||
</a>
|
||
<a href="#" class="nav-item" data-section="historical-analysis">
|
||
<span class="nav-icon">📈</span>
|
||
<span class="nav-text">Historical Analysis</span>
|
||
</a>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<div class="main-content">
|
||
<!-- Export Button -->
|
||
<button class="export-button" onclick="exportData()">Export</button>
|
||
|
||
<!-- Header -->
|
||
<div class="header">
|
||
<h1>OpenShift Resource Governance Tool</h1>
|
||
</div>
|
||
|
||
<!-- Content -->
|
||
<div class="content">
|
||
<!-- Dashboard Section -->
|
||
<div id="dashboardSection">
|
||
<!-- Cluster Overview -->
|
||
<div class="card">
|
||
<h2>🎯 Resource Governance Dashboard</h2>
|
||
|
||
<!-- Status Overview -->
|
||
<div class="status-overview">
|
||
<div class="status-icon" id="clusterStatusIcon">🟢</div>
|
||
<div class="status-content">
|
||
<h3 id="clusterStatus">Cluster Healthy</h3>
|
||
<p id="clusterStatusMessage">All systems operational</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Metrics Grid -->
|
||
<div class="metrics-grid">
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="totalPods">-</div>
|
||
<div class="metric-label">Total Pods</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="totalNamespaces">-</div>
|
||
<div class="metric-label">Namespaces</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="totalNodes">-</div>
|
||
<div class="metric-label">Nodes</div>
|
||
</div>
|
||
<div class="metric-card error" id="criticalIssuesCard">
|
||
<div class="metric-value" id="criticalIssues">-</div>
|
||
<div class="metric-label">Critical Issues</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Overcommit Summary -->
|
||
<div class="card">
|
||
<h3>📊 Cluster Overcommit Summary</h3>
|
||
<div class="metrics-grid">
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="cpuOvercommit">-</div>
|
||
<div class="metric-label">CPU Overcommit <span class="info-icon" onclick="showOvercommitDetails('cpu')">ℹ️</span></div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="memoryOvercommit">-</div>
|
||
<div class="metric-label">Memory Overcommit <span class="info-icon" onclick="showOvercommitDetails('memory')">ℹ️</span></div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="namespacesInOvercommit">-</div>
|
||
<div class="metric-label">Namespaces in Overcommit</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value" id="resourceUtilization">-</div>
|
||
<div class="metric-label">Resource Utilization <span class="info-icon" onclick="showResourceUtilizationDetails()">ℹ️</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Problem Summary -->
|
||
<div class="card problem-summary">
|
||
<h2>🔍 Problem Summary</h2>
|
||
<p style="margin-bottom: 1.5rem; color: #7f8c8d;">Identify namespaces with resource configuration issues and take action</p>
|
||
|
||
<!-- Filters -->
|
||
<div class="filters">
|
||
<div class="filter-group">
|
||
<label for="severityFilter">Severity:</label>
|
||
<select id="severityFilter">
|
||
<option value="all">All</option>
|
||
<option value="critical">Critical</option>
|
||
<option value="error">Error</option>
|
||
<option value="warning">Warning</option>
|
||
<option value="info">Info</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label for="perPageFilter">Per page:</label>
|
||
<select id="perPageFilter">
|
||
<option value="10">10</option>
|
||
<option value="20" selected>20</option>
|
||
<option value="50">50</option>
|
||
<option value="100">100</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-checkbox">
|
||
<input type="checkbox" id="includeSystemNamespaces">
|
||
<label for="includeSystemNamespaces">Include system namespaces</label>
|
||
</div>
|
||
<button class="apply-filters-btn" onclick="applyFilters()">Apply Filters</button>
|
||
</div>
|
||
|
||
<!-- Problem Table -->
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Namespace</th>
|
||
<th>Pods</th>
|
||
<th>Issues</th>
|
||
<th>Severity</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="problemTableBody">
|
||
<tr>
|
||
<td colspan="5" class="loading">Loading data...</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Actions -->
|
||
<div class="card">
|
||
<h2>⚡ Quick Actions</h2>
|
||
<div class="quick-actions">
|
||
<button class="quick-action-btn" onclick="generateVPARecommendations()">
|
||
<span>🎯</span>
|
||
<span>Generate VPA Recommendations</span>
|
||
</button>
|
||
<button class="quick-action-btn" onclick="showHistoricalAnalysis()">
|
||
<span>📈</span>
|
||
<span>Historical Analysis</span>
|
||
</button>
|
||
<button class="quick-action-btn" onclick="exportComplianceReport()">
|
||
<span>📊</span>
|
||
<span>Export Compliance Report</span>
|
||
</button>
|
||
<button class="quick-action-btn" onclick="fixAllIssues()">
|
||
<span>🔧</span>
|
||
<span>Fix All Issues</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Smart Recommendations Section -->
|
||
<div id="smartRecommendationsSection" style="display: none;">
|
||
<div class="card">
|
||
<h2>🎯 Smart Recommendations</h2>
|
||
<div id="smartRecommendationsContent">
|
||
<p class="loading">Loading recommendations...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VPA Management Section -->
|
||
<div id="vpaManagementSection" style="display: none;">
|
||
<div class="card">
|
||
<h2>⚙️ VPA Management</h2>
|
||
<div id="vpaManagementContent">
|
||
<p class="loading">Loading VPA data...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Historical Analysis Section -->
|
||
<div id="historicalAnalysisSection" style="display: none;">
|
||
<div class="card">
|
||
<h2>📈 Historical Analysis</h2>
|
||
<div id="historicalAnalysisContent">
|
||
<p class="loading">Loading historical data...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Global variables
|
||
let currentData = null;
|
||
let currentFilters = {
|
||
severity: 'all',
|
||
perPage: 20,
|
||
includeSystem: false
|
||
};
|
||
|
||
// Initialize the application
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadDashboard();
|
||
setupEventListeners();
|
||
});
|
||
|
||
// Setup event listeners
|
||
function setupEventListeners() {
|
||
// Navigation
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
const section = this.dataset.section;
|
||
showSection(section);
|
||
});
|
||
});
|
||
|
||
// Filter changes
|
||
document.getElementById('severityFilter').addEventListener('change', function() {
|
||
currentFilters.severity = this.value;
|
||
});
|
||
|
||
document.getElementById('perPageFilter').addEventListener('change', function() {
|
||
currentFilters.perPage = parseInt(this.value);
|
||
});
|
||
|
||
document.getElementById('includeSystemNamespaces').addEventListener('change', function() {
|
||
currentFilters.includeSystem = this.checked;
|
||
});
|
||
}
|
||
|
||
// Show specific section
|
||
function showSection(section) {
|
||
// Hide all sections
|
||
document.querySelectorAll('[id$="Section"]').forEach(sec => {
|
||
sec.style.display = 'none';
|
||
});
|
||
|
||
// Remove active class from all nav items
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
|
||
// Show selected section
|
||
document.getElementById(section + 'Section').style.display = 'block';
|
||
document.querySelector(`[data-section="${section}"]`).classList.add('active');
|
||
|
||
// Load section data
|
||
switch(section) {
|
||
case 'dashboard':
|
||
loadDashboard();
|
||
break;
|
||
case 'smart-recommendations':
|
||
loadSmartRecommendations();
|
||
break;
|
||
case 'vpa-management':
|
||
loadVPAManagement();
|
||
break;
|
||
case 'historical-analysis':
|
||
loadHistoricalAnalysis();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Load dashboard data
|
||
async function loadDashboard() {
|
||
try {
|
||
showLoading();
|
||
const response = await fetch('/api/v1/cluster/status');
|
||
const data = await response.json();
|
||
currentData = data;
|
||
updateDashboard(data);
|
||
} catch (error) {
|
||
console.error('Error loading dashboard:', error);
|
||
showError('Failed to load dashboard data');
|
||
}
|
||
}
|
||
|
||
// Update dashboard with data
|
||
function updateDashboard(data) {
|
||
// Update metrics
|
||
document.getElementById('totalPods').textContent = data.total_pods || 0;
|
||
document.getElementById('totalNamespaces').textContent = data.total_namespaces || 0;
|
||
document.getElementById('totalNodes').textContent = data.total_nodes || 0;
|
||
document.getElementById('criticalIssues').textContent = data.critical_issues || 0;
|
||
|
||
// Update overcommit metrics
|
||
if (data.overcommit) {
|
||
document.getElementById('cpuOvercommit').textContent = `${data.overcommit.cpu_overcommit_percent}%`;
|
||
document.getElementById('memoryOvercommit').textContent = `${data.overcommit.memory_overcommit_percent}%`;
|
||
document.getElementById('namespacesInOvercommit').textContent = data.overcommit.namespaces_in_overcommit || 0;
|
||
|
||
// Calculate resource utilization (usage vs requests)
|
||
const resourceUtilization = data.overcommit.resource_utilization || 0;
|
||
document.getElementById('resourceUtilization').textContent = `${resourceUtilization}%`;
|
||
|
||
// Store overcommit data for modal display
|
||
window.overcommitData = data.overcommit;
|
||
} else {
|
||
document.getElementById('cpuOvercommit').textContent = '0%';
|
||
document.getElementById('memoryOvercommit').textContent = '0%';
|
||
document.getElementById('namespacesInOvercommit').textContent = '0';
|
||
document.getElementById('resourceUtilization').textContent = '0%';
|
||
}
|
||
|
||
// Update status
|
||
const statusIcon = document.getElementById('clusterStatusIcon');
|
||
const statusText = document.getElementById('clusterStatus');
|
||
const statusMessage = document.getElementById('clusterStatusMessage');
|
||
|
||
if (data.critical_issues > 0) {
|
||
statusIcon.textContent = '🔴';
|
||
statusText.textContent = 'Critical Issues Found';
|
||
statusMessage.textContent = `${data.critical_issues} critical issues need attention`;
|
||
} else if (data.total_warnings > 0) {
|
||
statusIcon.textContent = '🟡';
|
||
statusText.textContent = 'Issues Found';
|
||
statusMessage.textContent = `${data.total_warnings} warnings found`;
|
||
} else {
|
||
statusIcon.textContent = '🟢';
|
||
statusText.textContent = 'Cluster Healthy';
|
||
statusMessage.textContent = 'All systems operational';
|
||
}
|
||
|
||
// Update problem table
|
||
updateProblemTable(data);
|
||
}
|
||
|
||
// Update problem table
|
||
function updateProblemTable(data) {
|
||
const tbody = document.getElementById('problemTableBody');
|
||
|
||
if (!data.namespaces || data.namespaces.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #7f8c8d;">No data available</td></tr>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
data.namespaces.forEach(namespace => {
|
||
const severity = getHighestSeverity(namespace);
|
||
const severityClass = `severity-${severity}`;
|
||
|
||
html += `
|
||
<tr>
|
||
<td><strong>${namespace.namespace}</strong></td>
|
||
<td>${Object.keys(namespace.pods || {}).length}</td>
|
||
<td>${namespace.total_validations || 0}</td>
|
||
<td><span class="severity-badge ${severityClass}">${severity}</span></td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-primary btn-sm" onclick="analyzeNamespace('${namespace.namespace}')">Analyze</button>
|
||
<button class="btn btn-success btn-sm" onclick="fixNamespace('${namespace.namespace}')">Fix</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
tbody.innerHTML = html;
|
||
}
|
||
|
||
// Get highest severity from namespace
|
||
function getHighestSeverity(namespace) {
|
||
const breakdown = namespace.severity_breakdown || {};
|
||
if (breakdown.error > 0) return 'error';
|
||
if (breakdown.warning > 0) return 'warning';
|
||
if (breakdown.info > 0) return 'info';
|
||
return 'info';
|
||
}
|
||
|
||
// Analyze namespace - show detailed issues
|
||
function analyzeNamespace(namespaceName) {
|
||
if (!currentData || !currentData.namespaces) return;
|
||
|
||
const namespace = currentData.namespaces.find(ns => ns.namespace === namespaceName);
|
||
if (!namespace) return;
|
||
|
||
// Show details in modal
|
||
showNamespaceDetailsSimple(namespaceName);
|
||
}
|
||
|
||
|
||
// Show namespace details in modal
|
||
function showNamespaceDetailsSimple(namespaceName) {
|
||
// Create modal if it doesn't exist
|
||
let modal = document.getElementById('namespaceModal');
|
||
if (!modal) {
|
||
modal = document.createElement('div');
|
||
modal.id = 'namespaceModal';
|
||
modal.className = 'modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>📋 Namespace Analysis</h2>
|
||
<span class="close">×</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>
|
||
|
||
${data.promql_queries ? `
|
||
<div class="promql-queries" style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;">
|
||
<h4>🔍 PromQL Queries Used</h4>
|
||
<p style="color: #6c757d; margin-bottom: 15px;">Copy these queries to validate in OpenShift Console → Monitoring → Metrics:</p>
|
||
|
||
<div class="query-section">
|
||
<h5>Cluster Total Resources:</h5>
|
||
<div class="query-item">
|
||
<label>CPU Total:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.cluster_cpu_total}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.cluster_cpu_total}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
<div class="query-item">
|
||
<label>Memory Total:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.cluster_memory_total}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.cluster_memory_total}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="query-section">
|
||
<h5>Workload Resource Usage:</h5>
|
||
<div class="query-item">
|
||
<label>CPU Usage:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.cpu_usage}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.cpu_usage}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
<div class="query-item">
|
||
<label>Memory Usage:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.memory_usage}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.memory_usage}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="query-section">
|
||
<h5>Workload Resource Requests:</h5>
|
||
<div class="query-item">
|
||
<label>CPU Requests:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.cpu_requests}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.cpu_requests}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
<div class="query-item">
|
||
<label>Memory Requests:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.memory_requests}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.memory_requests}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="query-section">
|
||
<h5>Workload Resource Limits:</h5>
|
||
<div class="query-item">
|
||
<label>CPU Limits:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.cpu_limits}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.cpu_limits}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
<div class="query-item">
|
||
<label>Memory Limits:</label>
|
||
<div class="query-box">
|
||
<code>${data.promql_queries.memory_limits}</code>
|
||
<button onclick="copyToClipboard('${data.promql_queries.memory_limits}')" class="copy-btn">📋 Copy</button>
|
||
</div>
|
||
</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);
|
||
}
|
||
|
||
// Copy to clipboard function for PromQL queries
|
||
async function copyToClipboard(text) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
// Show a brief success message
|
||
const button = event.target;
|
||
const originalText = button.textContent;
|
||
button.textContent = '✅ Copied!';
|
||
button.style.background = '#28a745';
|
||
setTimeout(() => {
|
||
button.textContent = originalText;
|
||
button.style.background = '';
|
||
}, 2000);
|
||
} catch (err) {
|
||
console.error('Failed to copy text: ', err);
|
||
// Fallback for older browsers
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
|
||
const button = event.target;
|
||
const originalText = button.textContent;
|
||
button.textContent = '✅ Copied!';
|
||
button.style.background = '#28a745';
|
||
setTimeout(() => {
|
||
button.textContent = originalText;
|
||
button.style.background = '';
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
// Utility functions
|
||
function showLoading() {
|
||
document.getElementById('problemTableBody').innerHTML =
|
||
'<tr><td colspan="5" class="loading">Loading data...</td></tr>';
|
||
}
|
||
|
||
function showError(message) {
|
||
document.getElementById('problemTableBody').innerHTML =
|
||
`<tr><td colspan="5" style="text-align: center; color: #e74c3c;">${message}</td></tr>`;
|
||
}
|
||
|
||
function showOvercommitDetails(type) {
|
||
if (!window.overcommitData) {
|
||
alert('Overcommit data not available');
|
||
return;
|
||
}
|
||
|
||
const data = window.overcommitData;
|
||
let title, content;
|
||
|
||
if (type === 'cpu') {
|
||
title = '🖥️ CPU Overcommit Details';
|
||
const cpuCapacity = data.cpu_capacity || 0;
|
||
const cpuRequests = data.cpu_requests || 0;
|
||
content = `
|
||
<div class="overcommit-details">
|
||
<h3>CPU Resource Analysis</h3>
|
||
<div class="metric-detail">
|
||
<strong>Capacity Total:</strong> ${cpuCapacity} cores
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Requests Total:</strong> ${cpuRequests} cores
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Overcommit:</strong> ${data.cpu_overcommit_percent}% (${cpuRequests} ÷ ${cpuCapacity} × 100)
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Available:</strong> ${(cpuCapacity - cpuRequests).toFixed(2)} cores
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (type === 'memory') {
|
||
title = '💾 Memory Overcommit Details';
|
||
const memoryCapacity = data.memory_capacity || 0;
|
||
const memoryRequests = data.memory_requests || 0;
|
||
const memoryCapacityGB = (memoryCapacity / (1024**3)).toFixed(1);
|
||
const memoryRequestsGB = (memoryRequests / (1024**3)).toFixed(1);
|
||
content = `
|
||
<div class="overcommit-details">
|
||
<h3>Memory Resource Analysis</h3>
|
||
<div class="metric-detail">
|
||
<strong>Capacity Total:</strong> ${memoryCapacity.toLocaleString()} bytes (≈ ${memoryCapacityGB} GB)
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Requests Total:</strong> ${memoryRequests.toLocaleString()} bytes (≈ ${memoryRequestsGB} GB)
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Overcommit:</strong> ${data.memory_overcommit_percent}% (${memoryRequestsGB} ÷ ${memoryCapacityGB} × 100)
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Available:</strong> ${((memoryCapacity - memoryRequests) / (1024**3)).toFixed(1)} GB
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>${title}</h2>
|
||
<span class="close">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
${content}
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
modal.style.display = 'block';
|
||
|
||
// Add close functionality
|
||
const closeBtn = modal.querySelector('.close');
|
||
closeBtn.onclick = () => modal.remove();
|
||
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
|
||
function showResourceUtilizationDetails() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>📊 Resource Utilization Details</h2>
|
||
<span class="close">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="overcommit-details">
|
||
<h3>Resource Utilization Analysis</h3>
|
||
<div class="metric-detail">
|
||
<strong>Current Status:</strong> Placeholder Implementation
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Purpose:</strong> Shows actual resource usage vs. requested resources across the cluster
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Formula:</strong> (Total Usage ÷ Total Requests) × 100
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Current Value:</strong> 75% (simulated data)
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Implementation Status:</strong>
|
||
<span style="color: #f39c12;">⚠️ Phase 2 - Smart Recommendations Engine</span>
|
||
</div>
|
||
<div class="metric-detail">
|
||
<strong>Next Steps:</strong>
|
||
<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">
|
||
<li>Integrate real Prometheus usage metrics</li>
|
||
<li>Calculate actual cluster-wide resource utilization</li>
|
||
<li>Provide insights on resource efficiency</li>
|
||
<li>Identify over/under-provisioned workloads</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
modal.style.display = 'block';
|
||
|
||
// Add close functionality
|
||
const closeBtn = modal.querySelector('.close');
|
||
closeBtn.onclick = () => modal.remove();
|
||
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
|
||
function closeModal() {
|
||
const modals = document.querySelectorAll('.modal');
|
||
modals.forEach(modal => modal.remove());
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|