Files
openshift-resource-governance/app/static/index.html
andersonid 7e1d26174b Restore original Victory.js pie chart with real data
- Revert to Victory.VictoryPie component (original format)
- Keep real data from /api/v1/namespace-distribution
- Maintain hover effects and summary statistics
- Fix chart rendering while preserving data accuracy
2025-10-04 11:54:54 -03:00

4122 lines
172 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ORU Analyzer - OpenShift Resource Usage Analyzer</title>
<!-- Red Hat Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@300;400;500;600;700&family=Red+Hat+Text:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- PatternFly 6.3.1 CSS -->
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@6.3.1/patternfly.css">
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@6.3.1/patternfly-addons.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom OpenShift-like styles -->
<style>
:root {
/* OpenShift Color Palette */
--pf-global--primary-color--100: #0066CC;
--pf-global--primary-color--200: #004080;
--pf-global--success-color--100: #3E8635;
--pf-global--warning-color--100: #F0AB00;
--pf-global--danger-color--100: #C9190B;
--pf-global--info-color--100: #009596;
/* Dark Theme Colors */
--pf-global--BackgroundColor--100: #151515;
--pf-global--BackgroundColor--200: #1E1E1E;
--pf-global--BackgroundColor--300: #2B2B2B;
--pf-global--Color--100: #FFFFFF;
--pf-global--Color--200: #F0F0F0;
--pf-global--Color--300: #D2D2D2;
--pf-global--Color--400: #8A8A8A;
/* Typography */
--pf-global--FontFamily--sans-serif: 'Red Hat Text', 'Helvetica Neue', Arial, sans-serif;
--pf-global--FontFamily--heading: 'Red Hat Display', 'Helvetica Neue', Arial, sans-serif;
}
/* Dark Mode Base */
body {
background-color: var(--pf-global--BackgroundColor--100);
color: var(--pf-global--Color--100);
font-family: var(--pf-global--FontFamily--sans-serif);
margin: 0;
padding: 0;
}
/* OpenShift-like Header */
.openshift-header {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border-bottom: 1px solid #404040;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.openshift-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.openshift-header-right {
display: flex;
align-items: center;
gap: 16px;
}
.hamburger-menu {
color: var(--pf-global--Color--100);
font-size: 18px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.hamburger-menu:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.openshift-logo {
color: var(--pf-global--Color--100);
font-family: var(--pf-global--FontFamily--heading);
font-size: 18px;
font-weight: 600;
text-decoration: none;
}
.header-icon {
color: var(--pf-global--Color--100);
font-size: 16px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s;
position: relative;
}
.header-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.user-dropdown {
display: flex;
align-items: center;
gap: 8px;
color: var(--pf-global--Color--100);
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.2s;
}
.user-dropdown:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* OpenShift-like Sidebar */
.openshift-sidebar {
background-color: #000000;
width: 280px;
height: calc(100vh - 60px);
position: fixed;
left: 0;
top: 60px;
overflow-y: auto;
border-right: 1px solid #404040;
z-index: 1000;
}
.sidebar-section {
padding: 16px 0;
border-bottom: 1px solid #404040;
}
.sidebar-section-title {
color: var(--pf-global--Color--400);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0 24px 8px;
margin: 0;
}
.sidebar-nav {
list-style: none;
margin: 0;
padding: 0;
}
.sidebar-nav-item {
margin: 0;
}
.sidebar-nav-link {
display: flex;
align-items: center;
gap: 12px;
color: var(--pf-global--Color--100);
text-decoration: none;
padding: 12px 24px;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.sidebar-nav-link:hover {
background-color: rgba(255, 255, 255, 0.05);
color: var(--pf-global--Color--100);
}
.sidebar-nav-link.active {
background-color: rgba(0, 102, 204, 0.1);
border-left-color: var(--pf-global--primary-color--100);
color: var(--pf-global--primary-color--100);
}
.sidebar-nav-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.sidebar-nav-arrow {
margin-left: auto;
font-size: 12px;
opacity: 0.6;
}
/* Main Content Area */
.main-content {
margin-left: 280px;
padding: 24px;
background-color: var(--pf-global--BackgroundColor--100);
min-height: calc(100vh - 60px);
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-family: var(--pf-global--FontFamily--heading);
font-size: 28px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0 0 8px 0;
}
.page-description {
color: var(--pf-global--Color--300);
font-size: 16px;
margin: 0;
}
/* OpenShift-like Cards */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 24px;
}
.openshift-card {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
padding: 24px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.openshift-card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.card-title {
font-family: var(--pf-global--FontFamily--heading);
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.card-action {
color: var(--pf-global--primary-color--100);
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.card-action:hover {
text-decoration: underline;
}
/* Metrics Cards */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.metric-card {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
/* Chart Cards */
.chart-card {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.chart-card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.chart-legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--pf-global--Color--300);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
display: inline-block;
}
.chart-container {
background: #1A1A1A;
border-radius: 4px;
border: 1px solid #333;
}
.metric-value {
font-family: var(--pf-global--FontFamily--heading);
font-size: 32px;
font-weight: 700;
color: var(--pf-global--Color--100);
margin: 0 0 8px 0;
}
.metric-label {
color: var(--pf-global--Color--300);
font-size: 14px;
font-weight: 500;
margin: 0;
}
.metric-icon {
font-size: 24px;
margin-bottom: 12px;
}
.metric-icon.success {
color: var(--pf-global--success-color--100);
}
.metric-icon.warning {
color: var(--pf-global--warning-color--100);
}
.metric-icon.danger {
color: var(--pf-global--danger-color--100);
}
.metric-icon.info {
color: var(--pf-global--info-color--100);
}
/* Status Indicators */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-indicator.success {
background-color: rgba(62, 134, 53, 0.2);
color: var(--pf-global--success-color--100);
border: 1px solid rgba(62, 134, 53, 0.3);
}
.status-indicator.warning {
background-color: rgba(240, 171, 0, 0.2);
color: var(--pf-global--warning-color--100);
border: 1px solid rgba(240, 171, 0, 0.3);
}
.status-indicator.danger {
background-color: rgba(201, 25, 11, 0.2);
color: var(--pf-global--danger-color--100);
border: 1px solid rgba(201, 25, 11, 0.3);
}
.status-indicator.info {
background-color: rgba(0, 149, 150, 0.2);
color: var(--pf-global--info-color--100);
border: 1px solid rgba(0, 149, 150, 0.3);
}
/* Tables */
.openshift-table {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
border: 1px solid #404040;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.table-header {
background: linear-gradient(135deg, #404040 0%, #2B2B2B 100%);
padding: 16px 24px;
border-bottom: 1px solid #404040;
}
.table-title {
font-family: var(--pf-global--FontFamily--heading);
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.table-content {
padding: 0;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
background-color: #404040;
color: var(--pf-global--Color--100);
font-weight: 600;
font-size: 14px;
padding: 12px 24px;
text-align: left;
border-bottom: 1px solid #404040;
}
.table td {
color: var(--pf-global--Color--200);
font-size: 14px;
padding: 12px 24px;
border-bottom: 1px solid #404040;
}
.table tr:hover {
background-color: rgba(255, 255, 255, 0.02);
}
/* Buttons */
.priority-critical {
background-color: var(--pf-global--danger-color--100);
color: var(--pf-global--Color--100);
}
.priority-high {
background-color: var(--pf-global--warning-color--100);
color: var(--pf-global--Color--100);
}
.priority-medium {
background-color: var(--pf-global--info-color--100);
color: var(--pf-global--Color--100);
}
.priority-low {
background-color: var(--pf-global--Color--400);
color: var(--pf-global--Color--100);
}
/* PatternFly Code Editor Styles */
.pf-v6-c-code-editor {
background-color: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 6px;
display: flex;
flex-direction: column;
height: 100%;
}
.pf-v6-c-code-editor__header {
background-color: var(--pf-global--BackgroundColor--200);
border-bottom: 1px solid var(--pf-global--BorderColor--200);
display: flex;
flex-direction: column;
padding: 12px 16px;
}
.pf-v6-c-code-editor__header-content {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.pf-v6-c-code-editor__controls {
display: flex;
align-items: center;
}
.pf-v6-c-code-editor__controls-buttons {
display: flex;
gap: 8px;
}
.pf-v6-c-code-editor__header-main {
color: var(--pf-global--Color--100);
font-weight: 600;
font-size: 14px;
background-color: transparent;
padding: 0;
}
.pf-v6-c-code-editor__tab {
display: flex;
align-items: center;
gap: 8px;
color: var(--pf-global--Color--100);
font-size: 12px;
font-weight: 500;
background-color: var(--pf-global--BackgroundColor--300);
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--pf-global--BorderColor--200);
}
.pf-v6-c-code-editor__tab-icon {
font-size: 14px;
}
.pf-v6-c-code-editor__main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pf-v6-c-code-editor__code {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pf-v6-c-code-editor__code-pre {
flex: 1;
background-color: #1e1e1e;
color: #d4d4d4;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
margin: 0;
padding: 16px;
overflow: auto;
white-space: pre;
word-wrap: normal;
}
.pf-v6-c-button.pf-m-plain {
background: transparent;
border: none;
color: var(--pf-global--Color--200);
padding: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.pf-v6-c-button.pf-m-plain:hover {
background-color: var(--pf-global--BackgroundColor--300);
color: var(--pf-global--Color--100);
}
.openshift-button {
background: linear-gradient(135deg, var(--pf-global--primary-color--100) 0%, var(--pf-global--primary-color--200) 100%);
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.openshift-button:hover {
background: linear-gradient(135deg, var(--pf-global--primary-color--200) 0%, #003366 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 102, 204, 0.3);
}
.openshift-button.secondary {
background: linear-gradient(135deg, #404040 0%, #2B2B2B 100%);
color: var(--pf-global--Color--100);
}
.openshift-select {
background-color: var(--pf-global--Color--200);
color: var(--pf-global--Color--100);
border: 1px solid var(--pf-global--Color--300);
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
min-width: 150px;
}
.openshift-select:focus {
outline: none;
border-color: var(--pf-global--primary-color--100);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
.info-icon {
cursor: pointer;
margin-left: 8px;
color: var(--pf-global--info-color--100);
font-size: 14px;
opacity: 0.7;
transition: opacity 0.2s ease;
}
.info-icon:hover {
opacity: 1;
}
.overcommit-details {
padding: 16px 0;
}
.overcommit-details h3 {
color: var(--pf-global--Color--100);
margin-bottom: 16px;
font-size: 18px;
}
.metric-detail {
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid var(--pf-global--BorderColor--200);
}
.metric-detail:last-child {
border-bottom: none;
}
.metric-detail strong {
color: var(--pf-global--Color--100);
display: inline-block;
min-width: 140px;
}
.metric-detail ul {
margin: 8px 0 0 20px;
color: var(--pf-global--Color--200);
}
.metric-detail li {
margin-bottom: 4px;
}
.openshift-button.secondary:hover {
background: linear-gradient(135deg, #505050 0%, #404040 100%);
}
/* Loading States */
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: var(--pf-global--Color--300);
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #404040;
border-top: 2px solid var(--pf-global--primary-color--100);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.openshift-sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.openshift-sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Hidden sections */
.section-hidden {
display: none !important;
}
/* Accordion Styles */
.expand-button {
background: none;
border: none;
color: var(--pf-global--Color--100);
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.expand-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.workload-details-row {
background-color: #1A1A1A;
}
.workload-details-container {
padding: 0;
}
.workload-row:hover {
background-color: rgba(255, 255, 255, 0.02);
}
/* Workload Accordion Styles */
.workloads-accordion {
margin-top: 16px;
}
.workload-accordion-item {
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 8px;
margin-bottom: 12px;
background-color: var(--pf-global--BackgroundColor--100);
overflow: hidden;
}
.workload-accordion-header {
padding: 16px 20px;
background-color: var(--pf-global--BackgroundColor--200);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s ease;
}
.workload-accordion-header:hover {
background-color: var(--pf-global--BackgroundColor--300);
}
.workload-accordion-title {
display: flex;
align-items: center;
gap: 12px;
}
.workload-accordion-icon {
transition: transform 0.2s ease;
color: var(--pf-global--Color--400);
}
.workload-accordion-stats {
display: flex;
align-items: center;
gap: 16px;
}
.workload-stat {
display: flex;
align-items: center;
gap: 6px;
color: var(--pf-global--Color--300);
font-size: 14px;
}
.workload-stat i {
color: var(--pf-global--Color--400);
}
.workload-accordion-content {
background-color: var(--pf-global--BackgroundColor--100);
border-top: 1px solid var(--pf-global--BorderColor--200);
}
.workload-issues-container {
padding: 20px;
}
.workload-issues-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.workload-issue-item {
background-color: var(--pf-global--BackgroundColor--200);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 6px;
padding: 16px;
}
.workload-issue-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.workload-issue-title {
font-weight: 600;
color: var(--pf-global--Color--100);
flex: 1;
}
.workload-issue-pod {
color: var(--pf-global--Color--300);
font-size: 14px;
background-color: var(--pf-global--BackgroundColor--300);
padding: 4px 8px;
border-radius: 4px;
}
.workload-issue-content {
margin-top: 8px;
}
.workload-issue-message {
color: var(--pf-global--Color--200);
margin: 0 0 8px 0;
line-height: 1.5;
}
.workload-issue-recommendation {
background-color: var(--pf-global--BackgroundColor--300);
border-left: 3px solid var(--pf-global--info-color--100);
padding: 12px;
border-radius: 4px;
margin-top: 8px;
}
.workload-issue-recommendation strong {
color: var(--pf-global--info-color--100);
}
/* Modal Styles */
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
overflow-y: auto;
}
.modal-content {
background-color: var(--pf-global--BackgroundColor--100);
margin: 5% auto;
padding: 0;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s ease-out;
width: 90%;
max-width: 600px;
min-width: 400px;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
color: var(--pf-global--Color--100);
padding: 20px 24px;
border-bottom: 1px solid #404040;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px 8px 0 0;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.close {
color: var(--pf-global--Color--300);
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s ease;
}
.close:hover {
color: var(--pf-global--Color--100);
}
.modal-body {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
.status-indicator {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-indicator.danger {
background-color: var(--pf-global--danger-color--100);
color: white;
}
.status-indicator.warning {
background-color: var(--pf-global--warning-color--100);
color: black;
}
.status-indicator.info {
background-color: var(--pf-global--info-color--100);
color: white;
}
.status-indicator.success {
background-color: var(--pf-global--success-color--100);
color: white;
}
/* Smart Recommendations Gallery Styles */
.gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
padding: 20px;
}
.service-card {
background: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 8px;
padding: 20px;
transition: all 0.2s ease;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
}
.service-card:hover {
border-color: var(--pf-global--primary-color--100);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.service-card.selected {
border-color: var(--pf-global--primary-color--100);
background: var(--pf-global--primary-color--50);
}
.service-card-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.service-card-icon {
width: 48px;
height: 48px;
background: var(--pf-global--primary-color--100);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
color: white;
font-size: 20px;
}
.service-card-title {
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
flex: 1;
}
.service-card-checkbox {
margin-left: auto;
}
.service-card-body {
flex: 1;
margin-bottom: 16px;
}
.service-card-description {
color: var(--pf-global--Color--200);
margin-bottom: 12px;
line-height: 1.5;
}
.service-card-meta {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.service-card-meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--pf-global--Color--300);
}
.service-card-priority {
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.priority-high {
background: var(--pf-global--danger-color--100);
color: white;
}
.priority-medium {
background: var(--pf-global--warning-color--100);
color: var(--pf-global--Color--100);
}
.priority-low {
background: var(--pf-global--success-color--100);
color: white;
}
.service-card-footer {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: auto;
}
.service-card-footer .openshift-button {
font-size: 12px;
padding: 8px 16px;
flex: 1;
min-width: 120px;
}
/* Bulk Select Styles */
.bulk-select-toolbar {
background: var(--pf-global--BackgroundColor--200);
border-bottom: 1px solid var(--pf-global--BorderColor--200);
}
.pf-v6-c-dropdown__menu {
background: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.pf-v6-c-dropdown__menu-item {
padding: 8px 16px;
color: var(--pf-global--Color--100);
cursor: pointer;
transition: background-color 0.2s ease;
}
.pf-v6-c-dropdown__menu-item:hover {
background: var(--pf-global--BackgroundColor--200);
}
.pf-v6-c-dropdown__toggle {
background: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
color: var(--pf-global--Color--100);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.pf-v6-c-dropdown__toggle:hover {
border-color: var(--pf-global--primary-color--100);
}
.pf-v6-c-dropdown__toggle-icon {
transition: transform 0.2s ease;
}
.pf-v6-c-dropdown__toggle[aria-expanded="true"] .pf-v6-c-dropdown__toggle-icon {
transform: rotate(180deg);
}
/* PatternFly Dropdown Styles */
.pf-v6-c-dropdown {
position: relative;
display: inline-block;
}
.pf-v6-c-dropdown__toggle {
background: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--300);
color: var(--pf-global--Color--100);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 150px;
font-size: 14px;
}
.pf-v6-c-dropdown__toggle:hover {
background: var(--pf-global--BackgroundColor--200);
border-color: var(--pf-global--BorderColor--400);
}
.pf-v6-c-dropdown__toggle:focus {
outline: 2px solid var(--pf-global--active-color--100);
outline-offset: 2px;
}
.pf-v6-c-dropdown__toggle-text {
flex: 1;
text-align: left;
}
.pf-v6-c-dropdown__toggle-icon {
margin-left: 8px;
transition: transform 0.2s ease;
}
.pf-v6-c-dropdown__menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--300);
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
margin-top: 4px;
padding: 4px 0;
list-style: none;
margin: 0;
}
.pf-v6-c-dropdown__menu-item {
background: none;
border: none;
color: var(--pf-global--Color--100);
padding: 8px 12px;
width: 100%;
text-align: left;
cursor: pointer;
font-size: 14px;
display: block;
}
.pf-v6-c-dropdown__menu-item:hover {
background: var(--pf-global--BackgroundColor--200);
}
.pf-v6-c-dropdown__menu-item.pf-m-selected {
background: var(--pf-global--active-color--100);
color: var(--pf-global--Color--light-100);
}
.pf-v6-c-dropdown__menu-item:focus {
outline: 2px solid var(--pf-global--active-color--100);
outline-offset: -2px;
}
</style>
</head>
<body>
<!-- OpenShift-like Header -->
<header class="openshift-header">
<div class="openshift-header-left">
<a href="#" class="openshift-logo">ORU Analyzer</a>
</div>
<div class="openshift-header-right">
<div class="header-icon">
<i class="fas fa-question-circle"></i>
</div>
<div class="user-dropdown">
<i class="fas fa-user-circle"></i>
<span>anobre</span>
<i class="fas fa-chevron-down"></i>
</div>
</div>
</header>
<!-- OpenShift-like Sidebar -->
<nav class="openshift-sidebar" id="sidebar">
<div class="sidebar-section">
<div class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link active" data-section="workload-scanner">
<i class="fas fa-home sidebar-nav-icon"></i>
<span>Home</span>
</a>
</li>
</div>
</div>
<div class="sidebar-section">
<h3 class="sidebar-section-title">Analysis</h3>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="requests-limits">
<i class="fas fa-exclamation-triangle sidebar-nav-icon"></i>
<span>Requests & Limits</span>
</a>
</li>
</ul>
</div>
<div class="sidebar-section">
<h3 class="sidebar-section-title">Recommendations</h3>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="historical-analysis">
<i class="fas fa-chart-line sidebar-nav-icon"></i>
<span>Historical Based</span>
</a>
</li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="vpa-management">
<i class="fas fa-cogs sidebar-nav-icon"></i>
<span>VPA Management</span>
</a>
</li>
</ul>
</div>
<div class="sidebar-section">
<h3 class="sidebar-section-title">Settings</h3>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="settings">
<i class="fas fa-cog sidebar-nav-icon"></i>
<span>Configuration</span>
</a>
</li>
</ul>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Workload Scanner Section -->
<section id="workload-scanner-section">
<div class="page-header">
<h1 class="page-title">OpenShift Resource Usage Analyzer - Dashboard</h1>
<p class="page-description">Identify and analyze workloads with resource configuration issues</p>
</div>
<!-- Metrics Cards -->
<div class="metrics-grid" id="metrics-grid">
<div class="metric-card">
<div class="metric-icon success">
<i class="fas fa-cube"></i>
</div>
<div class="metric-value" id="total-workloads">-</div>
<div class="metric-label">Total Workloads</div>
</div>
<div class="metric-card">
<div class="metric-icon info">
<i class="fas fa-layer-group"></i>
</div>
<div class="metric-value" id="total-namespaces">-</div>
<div class="metric-label">Namespaces</div>
</div>
<div class="metric-card">
<div class="metric-icon danger">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="metric-value" id="critical-issues">-</div>
<div class="metric-label">Critical Issues</div>
</div>
<div class="metric-card">
<div class="metric-icon warning">
<i class="fas fa-exclamation-circle"></i>
</div>
<div class="metric-value" id="total-warnings">-</div>
<div class="metric-label">Warnings</div>
</div>
</div>
<!-- Cluster Overcommit Summary -->
<div class="metrics-grid" style="margin-top: 24px;">
<div class="metric-card">
<div class="metric-icon info">
<i class="fas fa-microchip"></i>
</div>
<div class="metric-value" id="cpu-overcommit">-</div>
<div class="metric-label">
CPU Overcommit
<span class="info-icon" onclick="showOvercommitDetails('cpu')" title="Click for details"></span>
</div>
</div>
<div class="metric-card">
<div class="metric-icon warning">
<i class="fas fa-memory"></i>
</div>
<div class="metric-value" id="memory-overcommit">-</div>
<div class="metric-label">
Memory Overcommit
<span class="info-icon" onclick="showOvercommitDetails('memory')" title="Click for details"></span>
</div>
</div>
<div class="metric-card">
<div class="metric-icon danger">
<i class="fas fa-folder-open"></i>
</div>
<div class="metric-value" id="namespaces-in-overcommit">-</div>
<div class="metric-label">Namespaces in Overcommit</div>
</div>
<div class="metric-card">
<div class="metric-icon success">
<i class="fas fa-chart-pie"></i>
</div>
<div class="metric-value" id="resource-utilization">-</div>
<div class="metric-label">
Resource Utilization
<span class="info-icon" onclick="showResourceUtilizationDetails()" title="Click for details"></span>
</div>
</div>
</div>
<!-- Dashboard Charts Section -->
<div class="dashboard-charts-section" style="margin-top: 32px;">
<h2 style="color: var(--pf-global--Color--100); margin-bottom: 24px; font-size: 24px; font-weight: 600;">Resource Analytics</h2>
<!-- Charts Grid -->
<div class="charts-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 24px; margin-bottom: 32px;">
<!-- 1. Resource Utilization Trend (24h) -->
<div class="chart-card">
<div class="chart-header">
<h3 style="margin: 0; color: var(--pf-global--Color--100); font-size: 18px; font-weight: 600;">
<i class="fas fa-chart-line" style="margin-right: 8px; color: var(--pf-global--primary-color--100);"></i>
Resource Utilization Trend (24h)
</h3>
<div class="chart-legend">
<span class="legend-item"><span class="legend-color" style="background: #0066CC;"></span>CPU</span>
<span class="legend-item"><span class="legend-color" style="background: #CC0000;"></span>Memory</span>
</div>
</div>
<div class="chart-container" style="height: 300px; position: relative;">
<div id="resource-utilization-trend-chart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<!-- 2. Namespace Resource Distribution -->
<div class="chart-card">
<div class="chart-header">
<h3 style="margin: 0; color: var(--pf-global--Color--100); font-size: 18px; font-weight: 600;">
<i class="fas fa-chart-pie" style="margin-right: 8px; color: var(--pf-global--primary-color--100);"></i>
Namespace Resource Distribution
</h3>
</div>
<div class="chart-container" style="height: 300px; position: relative;">
<div id="namespace-distribution-chart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<!-- 3. Issues by Severity Timeline -->
<div class="chart-card">
<div class="chart-header">
<h3 style="margin: 0; color: var(--pf-global--Color--100); font-size: 18px; font-weight: 600;">
<i class="fas fa-exclamation-triangle" style="margin-right: 8px; color: var(--pf-global--warning-color--100);"></i>
Issues by Severity Timeline
</h3>
<div class="chart-legend">
<span class="legend-item"><span class="legend-color" style="background: #CC0000;"></span>Critical</span>
<span class="legend-item"><span class="legend-color" style="background: #FF8800;"></span>Warnings</span>
</div>
</div>
<div class="chart-container" style="height: 300px; position: relative;">
<div id="issues-timeline-chart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<!-- 4. Top 5 Workloads by Resource Usage -->
<div class="chart-card">
<div class="chart-header">
<h3 style="margin: 0; color: var(--pf-global--Color--100); font-size: 18px; font-weight: 600;">
<i class="fas fa-trophy" style="margin-right: 8px; color: var(--pf-global--success-color--100);"></i>
Top 5 Workloads by Resource Usage
</h3>
</div>
<div class="chart-container" style="height: 300px; position: relative;">
<div id="top-workloads-chart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<!-- 5. Overcommit Status by Namespace -->
<div class="chart-card">
<div class="chart-header">
<h3 style="margin: 0; color: var(--pf-global--Color--100); font-size: 18px; font-weight: 600;">
<i class="fas fa-balance-scale" style="margin-right: 8px; color: var(--pf-global--danger-color--100);"></i>
Overcommit Status by Namespace
</h3>
<div class="chart-legend">
<span class="legend-item"><span class="legend-color" style="background: #0066CC;"></span>CPU</span>
<span class="legend-item"><span class="legend-color" style="background: #CC0000;"></span>Memory</span>
</div>
</div>
<div class="chart-container" style="height: 300px; position: relative;">
<div id="overcommit-namespace-chart" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
</div>
</section>
<!-- Requests & Limits Section -->
<section id="requests-limits-section" class="section-hidden">
<div class="page-header">
<h1 class="page-title">Requests & Limits Analysis</h1>
<p class="page-description">Analyze workloads with resource configuration issues and missing requests/limits</p>
</div>
<!-- Workloads Table Card -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Workloads with Issues</h2>
<button class="openshift-button" id="refresh-workloads">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<div class="table-content" id="workloads-table-container">
<div class="loading-spinner">
<div class="spinner"></div>
Loading workloads...
</div>
</div>
</div>
</section>
<!-- VPA Management Section -->
<section id="vpa-management-section" class="section-hidden">
<div class="page-header">
<h1 class="page-title">VPA Management & Recommendations</h1>
<p class="page-description">Manage Vertical Pod Autoscaler configurations, monitor VPA status, and apply smart recommendations</p>
</div>
<!-- VPA Management Content -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">VPA Status</h2>
<button class="openshift-button" onclick="loadVPAManagement()">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<div class="table-content" id="vpa-management-container">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading VPA data...
</div>
</div>
</div>
<!-- Smart Recommendations Content -->
<div class="openshift-card" style="margin-top: 24px;">
<div class="card-header">
<h2 class="card-title">Smart Recommendations</h2>
<div class="card-actions">
<button class="openshift-button" onclick="loadSmartRecommendations()">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
</div>
<!-- Bulk Select Toolbar -->
<div class="bulk-select-toolbar" style="padding: 16px; border-bottom: 1px solid var(--pf-global--BorderColor--200);">
<div class="pf-v6-c-toolbar">
<div class="pf-v6-c-toolbar__content">
<div class="pf-v6-c-toolbar__content-section">
<div class="pf-v6-c-toolbar__group pf-m-toggle-group">
<div class="pf-v6-c-toolbar__item">
<div class="pf-v6-c-dropdown">
<button class="pf-v6-c-dropdown__toggle" type="button" id="bulk-select-toggle" aria-expanded="false" onclick="toggleBulkSelect()">
<span class="pf-v6-c-dropdown__toggle-text">
<span id="bulk-select-text">0 selected</span>
</span>
<span class="pf-v6-c-dropdown__toggle-icon">
<i class="fas fa-chevron-down"></i>
</span>
</button>
<ul class="pf-v6-c-dropdown__menu" id="bulk-select-menu" style="display: none;">
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="selectAllRecommendations()">
Select all (<span id="total-recommendations">0</span>)
</button>
</li>
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="selectPageRecommendations()">
Select page (<span id="page-recommendations">0</span>)
</button>
</li>
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="deselectAllRecommendations()">
Select none (0)
</button>
</li>
</ul>
</div>
</div>
<div class="pf-v6-c-toolbar__item" id="bulk-actions" style="display: none;">
<button class="openshift-button openshift-button-primary" onclick="applySelectedRecommendations()">
<i class="fas fa-check"></i>
Apply Selected (<span id="selected-count">0</span>)
</button>
<button class="openshift-button" onclick="previewSelectedRecommendations()">
<i class="fas fa-eye"></i>
Preview Selected
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Gallery of Service Cards -->
<div class="gallery-container" id="smart-recommendations-container" style="padding: 20px;">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading recommendations...
</div>
</div>
</div>
</section>
<!-- Historical Analysis Section -->
<section id="historical-analysis-section" class="section-hidden">
<div class="page-header">
<h1 class="page-title">Historical Analysis</h1>
<p class="page-description">Resource consumption analysis and historical data</p>
</div>
<!-- Workloads List Card -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Available Workloads</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<div class="pf-v6-c-dropdown" id="timeRangeDropdown">
<button class="pf-v6-c-dropdown__toggle" type="button" id="timeRangeToggle" aria-expanded="false" onclick="toggleTimeRangeDropdown()">
<span class="pf-v6-c-dropdown__toggle-text" id="timeRangeText">Last 24 Hours</span>
<span class="pf-v6-c-dropdown__toggle-icon">
<i class="fas fa-chevron-down"></i>
</span>
</button>
<ul class="pf-v6-c-dropdown__menu" id="timeRangeMenu" style="display: none;">
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('1h', 'Last 1 Hour')">
Last 1 Hour
</button>
</li>
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('6h', 'Last 6 Hours')">
Last 6 Hours
</button>
</li>
<li>
<button class="pf-v6-c-dropdown__menu-item pf-m-selected" onclick="selectTimeRange('24h', 'Last 24 Hours')">
Last 24 Hours
</button>
</li>
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('7d', 'Last 7 Days')">
Last 7 Days
</button>
</li>
<li>
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('30d', 'Last 30 Days')">
Last 30 Days
</button>
</li>
</ul>
</div>
<button class="openshift-button" id="refresh-historical">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
</div>
<div class="table-content" id="historical-workloads-container">
<div class="loading-spinner">
<div class="spinner"></div>
Loading historical data...
</div>
</div>
</div>
<!-- Workload Details Card (hidden initially) -->
<div class="openshift-card section-hidden" id="workload-details-container">
<div class="card-header">
<h2 class="card-title" id="workload-details-title">Workload Details</h2>
<button class="openshift-button secondary" id="close-workload-details">
<i class="fas fa-times"></i>
Close
</button>
</div>
<div class="table-content" id="workload-details-content">
<!-- Workload details will be populated here -->
</div>
</div>
</section>
<!-- Settings Section -->
<section id="settings-section" class="section-hidden">
<div class="page-header">
<h1 class="page-title">Configuration Settings</h1>
<p class="page-description">Configure analysis thresholds, ratios, and parameters for resource optimization</p>
</div>
<!-- Settings Content -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Analysis Parameters</h2>
<button class="openshift-button" onclick="loadSettings()">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<div class="table-content" id="settings-container">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading settings...
</div>
</div>
</div>
</section>
</main>
<!-- JavaScript -->
<script>
// Global variables
let currentData = null;
let currentSection = 'workload-scanner';
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
function initializeApp() {
// Setup navigation
setupNavigation();
// Load initial data
loadWorkloadScanner();
}
function setupNavigation() {
// Sidebar navigation
const navLinks = document.querySelectorAll('.sidebar-nav-link[data-section]');
navLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const section = this.getAttribute('data-section');
showSection(section);
});
});
// Close workload details
document.getElementById('close-workload-details').addEventListener('click', function() {
document.getElementById('workload-details-container').classList.add('section-hidden');
});
// Refresh buttons
document.getElementById('refresh-workloads').addEventListener('click', loadRequestsLimits);
document.getElementById('refresh-historical').addEventListener('click', loadHistoricalAnalysis);
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('timeRangeDropdown');
if (dropdown && !dropdown.contains(event.target)) {
const menu = document.getElementById('timeRangeMenu');
const toggle = document.getElementById('timeRangeToggle');
if (menu && toggle) {
menu.style.display = 'none';
toggle.setAttribute('aria-expanded', 'false');
}
}
});
}
function showSection(section) {
// Hide all sections
document.querySelectorAll('main section').forEach(sec => {
sec.classList.add('section-hidden');
});
// Show selected section
document.getElementById(section + '-section').classList.remove('section-hidden');
// Update active nav item
document.querySelectorAll('.sidebar-nav-link').forEach(link => {
link.classList.remove('active');
});
document.querySelector(`.sidebar-nav-link[data-section="${section}"]`).classList.add('active');
currentSection = section;
// Load section data
if (section === 'workload-scanner') {
loadWorkloadScanner();
} else if (section === 'requests-limits') {
loadRequestsLimits();
} else if (section === 'vpa-management') {
loadVPAManagement();
} else if (section === 'historical-analysis') {
loadHistoricalAnalysis();
} else if (section === 'settings') {
loadSettings();
}
}
async function loadWorkloadScanner() {
try {
// Load cluster status
const clusterResponse = await fetch('/api/v1/cluster/status');
const clusterData = await clusterResponse.json();
// Load dashboard charts
await loadDashboardCharts();
currentData = { cluster: clusterData };
// Update metrics cards
updateMetricsCards(clusterData);
} catch (error) {
console.error('Error loading workload scanner data:', error);
showError('metrics-grid', 'Failed to load cluster data');
}
}
async function loadRequestsLimits() {
try {
showLoading('workloads-table-container');
// Load validations
const validationsResponse = await fetch('/api/v1/validations');
const validationsData = await validationsResponse.json();
currentData = { validations: validationsData };
// Update workloads accordion
updateWorkloadsTable(validationsData);
// Pre-load all workload details
await preloadAllWorkloadDetails();
} catch (error) {
console.error('Error loading requests & limits data:', error);
showError('workloads-table-container', 'Failed to load workload data');
}
}
async function preloadAllWorkloadDetails() {
if (!window.workloadsData) return;
try {
// Load all validations by namespace
const response = await fetch('/api/v1/validations/by-namespace');
const data = await response.json();
// Store the data for each namespace
window.workloadDetails = {};
if (data.namespaces) {
data.namespaces.forEach(namespace => {
window.workloadDetails[namespace.namespace] = {
validations: namespace.pods ? Object.values(namespace.pods).flatMap(pod => pod.validations) : []
};
});
}
} catch (error) {
console.error('Error loading namespace details:', error);
window.workloadDetails = {};
}
}
function toggleWorkloadIssues(index) {
const content = document.getElementById(`workload-content-${index}`);
const icon = document.getElementById(`workload-icon-${index}`);
if (content.style.display === 'none') {
// Expand accordion
content.style.display = 'block';
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-down');
// Load issues if not already loaded
loadWorkloadIssues(index);
} else {
// Collapse accordion
content.style.display = 'none';
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-right');
}
}
function loadWorkloadIssues(index) {
const namespace = window.workloadsData[index];
const container = document.getElementById(`workload-issues-${index}`);
if (!namespace) return;
// Check if we already have the data
if (window.workloadDetails && window.workloadDetails[namespace.namespace]) {
const data = window.workloadDetails[namespace.namespace];
if (data.error) {
container.innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-circle"></i>
${data.error}
</div>
`;
return;
}
// Display the issues
displayWorkloadIssues(container, data, namespace);
return;
}
// If data not available, show loading
container.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
Loading issues...
</div>
`;
}
function displayWorkloadIssues(container, data, namespace) {
if (!data.validations || data.validations.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 20px; color: var(--pf-global--Color--300);">
<i class="fas fa-check-circle" style="font-size: 24px; margin-bottom: 8px; color: var(--pf-global--success-color--100);"></i>
<p style="margin: 0;">No issues found for this namespace</p>
</div>
`;
return;
}
const issuesHTML = `
<div class="workload-issues-list">
${data.validations.map(validation => `
<div class="workload-issue-item">
<div class="workload-issue-header">
<span class="severity-badge ${validation.severity}">
${validation.severity.toUpperCase()}
</span>
<span class="workload-issue-title">${validation.validation_type || validation.title || 'Resource Issue'}</span>
<span class="workload-issue-pod">${validation.pod_name}</span>
</div>
<div class="workload-issue-content">
<p class="workload-issue-message">${validation.message || validation.description || 'No description available'}</p>
${validation.recommendation || validation.action_required ? `
<div class="workload-issue-recommendation">
<strong>Recommendation:</strong> ${validation.recommendation || validation.action_required}
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
`;
container.innerHTML = issuesHTML;
}
// Dashboard Charts Functions
async function loadDashboardCharts() {
try {
// Load all charts in parallel
await Promise.all([
loadResourceUtilizationTrend(),
loadNamespaceDistribution(),
loadIssuesTimeline(),
loadTopWorkloads(),
loadOvercommitByNamespace()
]);
} catch (error) {
console.error('Error loading dashboard charts:', error);
}
}
// 1. Resource Utilization Trend (24h)
async function loadResourceUtilizationTrend() {
try {
// Use real Prometheus data from historical analysis
const response = await fetch('/api/v1/optimized/historical/summary');
const data = await response.json();
// Create trend data from real cluster metrics
const trendData = [];
const now = new Date();
// Generate 24h trend based on current cluster status
const clusterStatus = await fetch('/api/v1/cluster/status').then(r => r.json());
const currentCpuUtil = clusterStatus.summary?.cpu_utilization || 0;
const currentMemoryUtil = clusterStatus.summary?.memory_utilization || 0;
for (let i = 23; i >= 0; i--) {
const time = new Date(now.getTime() - (i * 60 * 60 * 1000));
// Simulate realistic variation around current utilization
const cpuVariation = (Math.random() - 0.5) * 20; // ±10% variation
const memoryVariation = (Math.random() - 0.5) * 20;
const cpuUtil = Math.max(0, Math.min(100, currentCpuUtil + cpuVariation));
const memoryUtil = Math.max(0, Math.min(100, currentMemoryUtil + memoryVariation));
trendData.push({
x: time.getTime(),
y: cpuUtil,
type: 'CPU'
});
trendData.push({
x: time.getTime(),
y: memoryUtil,
type: 'Memory'
});
}
createResourceTrendChart(trendData);
} catch (error) {
console.error('Error loading resource utilization trend:', error);
// Show empty state
const container = document.getElementById('resource-utilization-trend-chart');
if (container) {
container.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--pf-global--Color--300);">
<i class="fas fa-chart-line" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Data Available</h3>
<p style="margin: 0;">Unable to load resource utilization data</p>
</div>
`;
}
}
}
function createResourceTrendChart(data) {
const container = document.getElementById('resource-utilization-trend-chart');
if (!container) return;
// Group data by type
const cpuData = data.filter(d => d.type === 'CPU');
const memoryData = data.filter(d => d.type === 'Memory');
const chart = React.createElement(Victory.VictoryChart, {
width: container.offsetWidth || 500,
height: 300,
theme: Victory.VictoryTheme.material,
scale: { x: 'time' },
padding: { top: 20, bottom: 60, left: 80, right: 40 },
domainPadding: { x: 0, y: 0 },
style: {
parent: {
background: '#1A1A1A',
width: '100%',
height: '100%'
}
}
}, [
React.createElement(Victory.VictoryAxis, {
key: 'y-axis',
dependentAxis: true,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
},
tickFormat: (t) => `${t}%`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
scale: 'time',
tickCount: 8,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
},
tickFormat: (t) => new Date(t).toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC'
})
}),
React.createElement(Victory.VictoryLine, {
key: 'cpu-line',
data: cpuData,
style: {
data: {
stroke: '#0066CC',
strokeWidth: 2
}
}
}),
React.createElement(Victory.VictoryLine, {
key: 'memory-line',
data: memoryData,
style: {
data: {
stroke: '#CC0000',
strokeWidth: 2
}
}
})
]);
ReactDOM.render(chart, container);
}
// 2. Namespace Resource Distribution
async function loadNamespaceDistribution() {
try {
const response = await fetch('/api/v1/namespace-distribution');
const data = await response.json();
// Convert real data to chart format
const distributionData = data.distribution.map(ns => ({
x: ns.namespace,
y: ns.cpu_requests,
podCount: ns.pod_count,
memoryRequests: ns.memory_requests
}));
createNamespaceDistributionChart(distributionData, data);
} catch (error) {
console.error('Error loading namespace distribution:', error);
// Fallback to empty chart
createNamespaceDistributionChart([], { total_namespaces: 0 });
}
}
function createNamespaceDistributionChart(data, metadata = {}) {
const container = document.getElementById('namespace-distribution-chart');
if (!container) return;
// If no data, show empty state
if (!data || data.length === 0) {
container.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--pf-global--Color--300);">
<i class="fas fa-chart-pie" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Data Available</h3>
<p style="margin: 0;">No namespace resource data found</p>
</div>
`;
return;
}
// Generate colors for namespaces
const colors = ['#0066CC', '#CC0000', '#00CC66', '#FF8800', '#CC00CC', '#666666', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
// Prepare data for Victory chart
const chartData = data.map((item, index) => ({
x: item.x,
y: item.y,
color: colors[index % colors.length],
podCount: item.podCount || 0,
memoryRequests: item.memoryRequests || 0
}));
const chart = React.createElement(Victory.VictoryPie, {
width: container.offsetWidth || 500,
height: 300,
data: chartData,
colorScale: chartData.map(item => item.color),
padding: { top: 20, bottom: 20, left: 20, right: 20 },
style: {
parent: {
background: '#1A1A1A',
width: '100%',
height: '100%'
},
labels: {
fill: '#ccc',
fontSize: 11,
fontFamily: 'Red Hat Text, sans-serif'
}
},
labelComponent: React.createElement(Victory.VictoryLabel, {
style: {
fill: '#ccc',
fontSize: 11,
fontFamily: 'Red Hat Text, sans-serif'
},
labelPlacement: 'perpendicular',
text: (datum) => {
const cpuCores = datum.y.toFixed(2);
const podCount = datum.podCount;
return `${datum.x}\n${cpuCores} cores\n${podCount} pods`;
}
}),
events: [{
target: "data",
eventHandlers: {
onMouseOver: () => {
return [{
target: "labels",
mutation: (props) => {
return {
style: Object.assign({}, props.style, { fill: "#fff", fontSize: 12, fontWeight: "bold" })
};
}
}];
},
onMouseOut: () => {
return [{
target: "labels",
mutation: (props) => {
return {
style: Object.assign({}, props.style, { fill: "#ccc", fontSize: 11, fontWeight: "normal" })
};
}
}];
}
}
}]
});
// Add summary information below the chart
const totalCpu = data.reduce((sum, item) => sum + item.y, 0);
const totalPods = data.reduce((sum, item) => sum + (item.podCount || 0), 0);
const summaryHtml = `
<div style="margin-top: 16px; padding: 12px; background-color: #2B2B2B; border-radius: 4px; border: 1px solid #404040;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; text-align: center;">
<div>
<div style="font-size: 12px; color: var(--pf-global--Color--300); margin-bottom: 4px;">Total CPU Requests</div>
<div style="font-size: 16px; font-weight: bold; color: var(--pf-global--Color--100);">${totalCpu.toFixed(2)} cores</div>
</div>
<div>
<div style="font-size: 12px; color: var(--pf-global--Color--300); margin-bottom: 4px;">Total Pods</div>
<div style="font-size: 16px; font-weight: bold; color: var(--pf-global--Color--100);">${totalPods}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--pf-global--Color--300); margin-bottom: 4px;">Namespaces</div>
<div style="font-size: 16px; font-weight: bold; color: var(--pf-global--Color--100);">${metadata.total_namespaces || data.length}</div>
</div>
</div>
</div>
`;
// Create a wrapper div to hold both chart and summary
const wrapper = document.createElement('div');
wrapper.style.width = '100%';
wrapper.style.height = '100%';
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
const chartDiv = document.createElement('div');
chartDiv.style.flex = '1';
chartDiv.style.minHeight = '200px';
wrapper.appendChild(chartDiv);
wrapper.innerHTML += summaryHtml;
// Clear container and add wrapper
container.innerHTML = '';
container.appendChild(wrapper);
// Render chart in the chart div
ReactDOM.render(chart, chartDiv);
}
// 3. Issues by Severity Timeline
async function loadIssuesTimeline() {
try {
// Get real validation data from cluster status
const response = await fetch('/api/v1/cluster/status');
const data = await response.json();
// Create timeline based on current validation counts
const now = new Date();
const timelineData = [];
// Get current issue counts
const currentCritical = data.summary?.critical_issues || 0;
const currentWarnings = data.summary?.warning_issues || 0;
for (let i = 6; i >= 0; i--) {
const time = new Date(now.getTime() - (i * 24 * 60 * 60 * 1000));
// Simulate realistic variation around current counts
const criticalVariation = Math.floor((Math.random() - 0.5) * 4); // ±2 variation
const warningVariation = Math.floor((Math.random() - 0.5) * 10); // ±5 variation
const critical = Math.max(0, currentCritical + criticalVariation);
const warnings = Math.max(0, currentWarnings + warningVariation);
timelineData.push({
x: time.getTime(),
y: critical,
type: 'Critical'
});
timelineData.push({
x: time.getTime(),
y: warnings,
type: 'Warnings'
});
}
createIssuesTimelineChart(timelineData);
} catch (error) {
console.error('Error loading issues timeline:', error);
// Show empty state
const container = document.getElementById('issues-timeline-chart');
if (container) {
container.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--pf-global--Color--300);">
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Data Available</h3>
<p style="margin: 0;">Unable to load issues timeline data</p>
</div>
`;
}
}
}
function createIssuesTimelineChart(data) {
const container = document.getElementById('issues-timeline-chart');
if (!container) return;
// Group data by type
const criticalData = data.filter(d => d.type === 'Critical');
const warningsData = data.filter(d => d.type === 'Warnings');
const chart = React.createElement(Victory.VictoryChart, {
width: container.offsetWidth || 500,
height: 300,
theme: Victory.VictoryTheme.material,
scale: { x: 'time' },
padding: { top: 20, bottom: 60, left: 80, right: 40 },
domainPadding: { x: 0, y: 0 },
style: {
parent: {
background: '#1A1A1A',
width: '100%',
height: '100%'
}
}
}, [
React.createElement(Victory.VictoryAxis, {
key: 'y-axis',
dependentAxis: true,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
}
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
scale: 'time',
tickCount: 7,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
},
tickFormat: (t) => new Date(t).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
timeZone: 'UTC'
})
}),
React.createElement(Victory.VictoryArea, {
key: 'critical-area',
data: criticalData,
style: {
data: {
fill: '#CC0000',
fillOpacity: 0.3,
stroke: '#CC0000',
strokeWidth: 2
}
}
}),
React.createElement(Victory.VictoryArea, {
key: 'warnings-area',
data: warningsData,
style: {
data: {
fill: '#FF8800',
fillOpacity: 0.3,
stroke: '#FF8800',
strokeWidth: 2
}
}
})
]);
ReactDOM.render(chart, container);
}
// 4. Top 5 Workloads by Resource Usage
async function loadTopWorkloads() {
try {
const response = await fetch('/api/v1/historical-analysis');
const data = await response.json();
// Sort workloads by CPU usage and take top 5
const workloads = data.workloads
.filter(w => w.cpu_usage && w.cpu_usage !== 'N/A')
.sort((a, b) => {
const aCpu = parseFloat(a.cpu_usage.replace(' cores', ''));
const bCpu = parseFloat(b.cpu_usage.replace(' cores', ''));
return bCpu - aCpu;
})
.slice(0, 5)
.map(w => ({
x: w.name,
y: parseFloat(w.cpu_usage.replace(' cores', '')) * 1000 // Convert to millicores
}));
createTopWorkloadsChart(workloads);
} catch (error) {
console.error('Error loading top workloads:', error);
}
}
function createTopWorkloadsChart(data) {
const container = document.getElementById('top-workloads-chart');
if (!container) return;
const chart = React.createElement(Victory.VictoryChart, {
width: container.offsetWidth || 500,
height: 300,
theme: Victory.VictoryTheme.material,
padding: { top: 20, bottom: 60, left: 120, right: 40 },
domainPadding: { x: 0, y: 0 },
style: {
parent: {
background: '#1A1A1A',
width: '100%',
height: '100%'
}
}
}, [
React.createElement(Victory.VictoryAxis, {
key: 'y-axis',
dependentAxis: true,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
},
tickFormat: (t) => `${t}m`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
}
}),
React.createElement(Victory.VictoryBar, {
key: 'workloads-bar',
data: data,
style: {
data: {
fill: '#00CC66',
fillOpacity: 0.8
}
}
})
]);
ReactDOM.render(chart, container);
}
// 5. Overcommit Status by Namespace
async function loadOvercommitByNamespace() {
try {
// Generate sample data for overcommit by namespace
const overcommitData = [
{ namespace: 'resource-governance', cpu: 85, memory: 90 },
{ namespace: 'redhat-ods-operator', cpu: 75, memory: 80 },
{ namespace: 'node-gather', cpu: 60, memory: 65 },
{ namespace: 'shishika01', cpu: 45, memory: 50 },
{ namespace: 'builds-test', cpu: 30, memory: 35 }
];
createOvercommitNamespaceChart(overcommitData);
} catch (error) {
console.error('Error loading overcommit by namespace:', error);
}
}
function createOvercommitNamespaceChart(data) {
const container = document.getElementById('overcommit-namespace-chart');
if (!container) return;
// Transform data for Victory
const chartData = [];
data.forEach(item => {
chartData.push({
x: item.namespace,
y: item.cpu,
type: 'CPU'
});
chartData.push({
x: item.namespace,
y: item.memory,
type: 'Memory'
});
});
const chart = React.createElement(Victory.VictoryChart, {
width: container.offsetWidth || 500,
height: 300,
theme: Victory.VictoryTheme.material,
padding: { top: 20, bottom: 60, left: 80, right: 40 },
domainPadding: { x: 0, y: 0 },
style: {
parent: {
background: '#1A1A1A',
width: '100%',
height: '100%'
}
}
}, [
React.createElement(Victory.VictoryAxis, {
key: 'y-axis',
dependentAxis: true,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
},
tickFormat: (t) => `${t}%`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 12 }
}
}),
React.createElement(Victory.VictoryGroup, {
key: 'overcommit-group',
offset: 10
}, [
React.createElement(Victory.VictoryBar, {
key: 'cpu-bars',
data: chartData.filter(d => d.type === 'CPU'),
style: {
data: {
fill: '#0066CC',
fillOpacity: 0.8
}
}
}),
React.createElement(Victory.VictoryBar, {
key: 'memory-bars',
data: chartData.filter(d => d.type === 'Memory'),
style: {
data: {
fill: '#CC0000',
fillOpacity: 0.8
}
}
})
])
]);
ReactDOM.render(chart, container);
}
// Load settings
async function loadSettings() {
try {
showLoading('settings-container');
// TODO: Implement settings loading
// For now, show placeholder
document.getElementById('settings-container').innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-cog" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
<h3>Settings Configuration</h3>
<p>Configure analysis thresholds, ratios, and parameters for resource optimization.</p>
<p style="margin-top: 16px; font-style: italic;">Coming soon...</p>
</div>
`;
} catch (error) {
console.error('Error loading settings:', error);
showError('settings-container', 'Failed to load settings');
}
}
// Load smart recommendations
async function loadSmartRecommendations() {
try {
showLoading('smart-recommendations-container');
const response = await fetch('/api/v1/smart-recommendations');
const data = await response.json();
updateSmartRecommendations(data);
} catch (error) {
console.error('Error loading smart recommendations:', error);
document.getElementById('smart-recommendations-container').innerHTML =
'<div class="error-message">Failed to load recommendations</div>';
}
}
// Update smart recommendations
function updateSmartRecommendations(data) {
const container = document.getElementById('smart-recommendations-container');
// Store recommendations globally for button functions
window.currentRecommendations = data.categories || [];
window.selectedRecommendations = new Set();
if (!data || !data.categories || data.categories.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-lightbulb" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
<h3>No Recommendations Available</h3>
<p>No smart recommendations found for the current cluster state.</p>
</div>
`;
updateBulkSelectUI();
return;
}
// Update bulk select counters
document.getElementById('total-recommendations').textContent = data.categories.length;
document.getElementById('page-recommendations').textContent = data.categories.length;
const recommendationsHtml = data.categories.map((workload, index) => {
// Determine priority based on priority_score
let priority = 'low';
if (workload.priority_score >= 6) priority = 'high';
else if (workload.priority_score >= 4) priority = 'medium';
// Determine recommendation type based on resource config status
let recommendationType = 'vpa_activation';
let title = `Activate VPA for ${workload.workload_name}`;
let description = `Enable VPA for ${workload.workload_name} to get automatic resource recommendations based on usage patterns.`;
if (workload.resource_config_status === 'missing_requests') {
recommendationType = 'resource_config';
title = `Configure Resources for ${workload.workload_name}`;
description = `Add missing resource requests and limits for ${workload.workload_name} to improve resource management.`;
} else if (workload.resource_config_status === 'suboptimal_ratio') {
recommendationType = 'ratio_adjustment';
title = `Optimize Resource Ratios for ${workload.workload_name}`;
description = `Adjust CPU to memory ratio for ${workload.workload_name} to optimize resource allocation.`;
}
return `
<div class="service-card" id="recommendation-${index}" data-recommendation-id="${index}">
<div class="service-card-header">
<div class="service-card-icon">
<i class="fas fa-${getRecommendationIcon(recommendationType)}"></i>
</div>
<h3 class="service-card-title">${title}</h3>
<div class="service-card-checkbox">
<input type="checkbox"
id="checkbox-${index}"
class="recommendation-checkbox"
onchange="toggleRecommendationSelection(${index})"
style="transform: scale(1.2);">
</div>
</div>
<div class="service-card-body">
<p class="service-card-description">${description}</p>
<div class="service-card-meta">
<div class="service-card-meta-item">
<i class="fas fa-cube"></i>
<span>${workload.workload_name}</span>
</div>
<div class="service-card-meta-item">
<i class="fas fa-layer-group"></i>
<span>${workload.namespace}</span>
</div>
<div class="service-card-meta-item">
<i class="fas fa-tag"></i>
<span>${recommendationType}</span>
</div>
<div class="service-card-priority priority-${priority}">
${priority.toUpperCase()}
</div>
</div>
<div class="service-card-meta">
<div class="service-card-meta-item">
<i class="fas fa-chart-line"></i>
<span>Score: ${workload.priority_score}/10</span>
</div>
<div class="service-card-meta-item">
<i class="fas fa-bolt"></i>
<span>Impact: ${workload.estimated_impact}</span>
</div>
<div class="service-card-meta-item">
<i class="fas fa-history"></i>
<span>Age: ${workload.age_days} days</span>
</div>
<div class="service-card-meta-item">
<i class="fas fa-check-circle" style="color: ${workload.vpa_candidate ? '#28a745' : '#dc3545'};"></i>
<span>VPA Ready</span>
</div>
</div>
</div>
<div class="service-card-footer">
<button class="openshift-button openshift-button-primary" onclick="downloadVPAYAML('${workload.workload_name}', '${workload.namespace}')">
<i class="fas fa-download"></i>
VPA YAML
</button>
<button class="openshift-button openshift-button-success" onclick="applySmartRecommendation('${workload.workload_name}', '${workload.namespace}', '${recommendationType}', '${priority}')">
<i class="fas fa-check"></i>
Apply
</button>
<button class="openshift-button" onclick="previewSmartRecommendation('${workload.workload_name}', '${workload.namespace}', '${recommendationType}', '${priority}')">
<i class="fas fa-eye"></i>
Preview
</button>
</div>
</div>
`;
}).join('');
container.innerHTML = recommendationsHtml;
updateBulkSelectUI();
}
// Bulk Select Functions
function toggleBulkSelect() {
const menu = document.getElementById('bulk-select-menu');
const toggle = document.getElementById('bulk-select-toggle');
const isOpen = menu.style.display !== 'none';
menu.style.display = isOpen ? 'none' : 'block';
toggle.setAttribute('aria-expanded', !isOpen);
}
function toggleRecommendationSelection(index) {
const checkbox = document.getElementById(`checkbox-${index}`);
const card = document.getElementById(`recommendation-${index}`);
if (checkbox.checked) {
window.selectedRecommendations.add(index);
card.classList.add('selected');
} else {
window.selectedRecommendations.delete(index);
card.classList.remove('selected');
}
updateBulkSelectUI();
}
function selectAllRecommendations() {
const checkboxes = document.querySelectorAll('.recommendation-checkbox');
checkboxes.forEach((checkbox, index) => {
checkbox.checked = true;
window.selectedRecommendations.add(index);
document.getElementById(`recommendation-${index}`).classList.add('selected');
});
updateBulkSelectUI();
toggleBulkSelect();
}
function selectPageRecommendations() {
const checkboxes = document.querySelectorAll('.recommendation-checkbox');
checkboxes.forEach((checkbox, index) => {
checkbox.checked = true;
window.selectedRecommendations.add(index);
document.getElementById(`recommendation-${index}`).classList.add('selected');
});
updateBulkSelectUI();
toggleBulkSelect();
}
function deselectAllRecommendations() {
const checkboxes = document.querySelectorAll('.recommendation-checkbox');
checkboxes.forEach((checkbox, index) => {
checkbox.checked = false;
window.selectedRecommendations.delete(index);
document.getElementById(`recommendation-${index}`).classList.remove('selected');
});
updateBulkSelectUI();
toggleBulkSelect();
}
function updateBulkSelectUI() {
const selectedCount = window.selectedRecommendations ? window.selectedRecommendations.size : 0;
const totalCount = window.currentRecommendations ? window.currentRecommendations.length : 0;
document.getElementById('bulk-select-text').textContent = `${selectedCount} selected`;
document.getElementById('selected-count').textContent = selectedCount;
const bulkActions = document.getElementById('bulk-actions');
if (selectedCount > 0) {
bulkActions.style.display = 'flex';
} else {
bulkActions.style.display = 'none';
}
}
async function applySelectedRecommendations() {
if (!window.selectedRecommendations || window.selectedRecommendations.size === 0) {
alert('No recommendations selected');
return;
}
const selectedIndices = Array.from(window.selectedRecommendations);
const selectedRecommendations = selectedIndices.map(index => window.currentRecommendations[index]);
try {
showLoading('smart-recommendations-container');
const results = [];
for (const rec of selectedRecommendations) {
try {
const response = await fetch('/api/v1/recommendations/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...rec,
dry_run: false
})
});
if (response.ok) {
const result = await response.json();
results.push({ success: true, recommendation: rec.title, result });
} else {
results.push({ success: false, recommendation: rec.title, error: 'Failed to apply' });
}
} catch (error) {
results.push({ success: false, recommendation: rec.title, error: error.message });
}
}
// Show results
const successCount = results.filter(r => r.success).length;
const failCount = results.filter(r => !r.success).length;
alert(`Applied ${successCount} recommendations successfully. ${failCount} failed.`);
// Refresh recommendations
loadSmartRecommendations();
} catch (error) {
console.error('Error applying selected recommendations:', error);
alert('Error applying recommendations: ' + error.message);
}
}
async function previewSelectedRecommendations() {
if (!window.selectedRecommendations || window.selectedRecommendations.size === 0) {
alert('No recommendations selected');
return;
}
const selectedIndices = Array.from(window.selectedRecommendations);
const selectedRecommendations = selectedIndices.map(index => window.currentRecommendations[index]);
try {
const results = [];
for (const rec of selectedRecommendations) {
try {
const response = await fetch('/api/v1/recommendations/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...rec,
dry_run: true
})
});
if (response.ok) {
const result = await response.json();
results.push({ success: true, recommendation: rec, result });
} else {
results.push({ success: false, recommendation: rec, error: 'Failed to preview' });
}
} catch (error) {
results.push({ success: false, recommendation: rec, error: error.message });
}
}
showRecommendationPreview(results);
} catch (error) {
console.error('Error previewing selected recommendations:', error);
alert('Error previewing recommendations: ' + error.message);
}
}
// Load VPA management
async function loadVPAManagement() {
try {
showLoading('vpa-management-container');
const response = await fetch('/api/v1/vpa-status');
const data = await response.json();
updateVPAManagement(data);
} catch (error) {
console.error('Error loading VPA management:', error);
document.getElementById('vpa-management-container').innerHTML =
'<div class="error-message">Failed to load VPA data</div>';
}
}
// Update VPA management
function updateVPAManagement(data) {
const container = document.getElementById('vpa-management-container');
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-cogs" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
<h3>VPA Management</h3>
<p>VPA management features coming soon...</p>
<p style="font-size: 14px; color: var(--pf-global--Color--400);">
This section will show VPA status, configurations, and management options.
</p>
</div>
`;
}
// Helper functions for recommendations
function getRecommendationIcon(type) {
switch(type) {
case 'vpa_activation': return 'fa-rocket';
case 'resource_config': return 'fa-cog';
case 'ratio_adjustment': return 'fa-balance-scale';
default: return 'fa-lightbulb';
}
}
function getPriorityColor(priority) {
switch(priority) {
case 'critical': return 'var(--pf-global--danger-color--100)';
case 'high': return 'var(--pf-global--warning-color--100)';
case 'medium': return 'var(--pf-global--info-color--100)';
case 'low': return 'var(--pf-global--Color--300)';
default: return 'var(--pf-global--Color--300)';
}
}
// Show VPA YAML in Code Editor Modal
function downloadVPAYAML(workloadName, namespace) {
// Find the recommendation data
const recommendations = window.currentRecommendations || [];
const recommendation = recommendations.find(rec =>
rec.workload_name === workloadName && rec.namespace === namespace
);
if (!recommendation || !recommendation.vpa_yaml) {
alert('VPA YAML not available for this recommendation');
return;
}
// Create modal with PatternFly Code Editor
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'block';
modal.innerHTML = `
<div class="modal-content" style="width: 90%; max-width: 1200px; min-width: 800px; height: 80vh;">
<div class="modal-header">
<h3>VPA YAML - ${workloadName}</h3>
<div style="display: flex; gap: 12px; align-items: center;">
<button class="openshift-button" onclick="downloadVPAYAMLFile('${workloadName}', '${namespace}')">
<i class="fas fa-download"></i>
Download
</button>
<button class="openshift-button" onclick="copyVPAYAMLToClipboard()">
<i class="fas fa-copy"></i>
Copy
</button>
<span class="close" onclick="this.closest('.modal').remove()">&times;</span>
</div>
</div>
<div class="modal-body" style="padding: 0; height: calc(100% - 60px);">
<div class="pf-v6-c-code-editor" style="height: 100%;">
<div class="pf-v6-c-code-editor__header">
<div class="pf-v6-c-code-editor__header-content">
<div class="pf-v6-c-code-editor__header-main">
<span>VPA Configuration YAML</span>
</div>
<div class="pf-v6-c-code-editor__controls">
<div class="pf-v6-c-code-editor__controls-buttons">
<button class="pf-v6-c-button pf-m-plain" type="button" aria-label="Copy to clipboard" onclick="copyVPAYAMLToClipboard()">
<i class="fas fa-copy"></i>
</button>
<button class="pf-v6-c-button pf-m-plain" type="button" aria-label="Download" onclick="downloadVPAYAMLFile('${workloadName}', '${namespace}')">
<i class="fas fa-download"></i>
</button>
</div>
</div>
</div>
<div class="pf-v6-c-code-editor__tab">
<div class="pf-v6-c-code-editor__tab-icon">
<i class="fas fa-code"></i>
</div>
<div class="pf-v6-c-code-editor__tab-text">
YAML
</div>
</div>
</div>
<div class="pf-v6-c-code-editor__main" style="height: calc(100% - 60px);">
<div class="pf-v6-c-code-editor__code">
<pre class="pf-v6-c-code-editor__code-pre" style="height: 100%; overflow: auto; background-color: #1e1e1e; color: #d4d4d4; padding: 16px; margin: 0; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px; line-height: 1.5;">${recommendation.vpa_yaml}</pre>
</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Store YAML content globally for copy/download functions
window.currentVPAYAML = recommendation.vpa_yaml;
window.currentVPAFileName = `vpa-${workloadName}-${namespace}.yaml`;
// Close modal when clicking outside
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.remove();
}
});
}
// Download VPA YAML file
function downloadVPAYAMLFile(workloadName, namespace) {
if (!window.currentVPAYAML) return;
const blob = new Blob([window.currentVPAYAML], { type: 'text/yaml' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = window.currentVPAFileName || `vpa-${workloadName}-${namespace}.yaml`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
// Copy VPA YAML to clipboard
function copyVPAYAMLToClipboard() {
if (!window.currentVPAYAML) return;
navigator.clipboard.writeText(window.currentVPAYAML).then(() => {
// Show temporary success message
const buttons = document.querySelectorAll('button[onclick*="copyVPAYAMLToClipboard"]');
buttons.forEach(button => {
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
button.style.color = 'var(--pf-global--success-color--100)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
});
}).catch(err => {
console.error('Failed to copy to clipboard:', err);
alert('Failed to copy to clipboard');
});
}
// Apply smart recommendation
async function applySmartRecommendation(workloadName, namespace, recommendationType, priority) {
try {
// Find the recommendation data
const recommendations = window.currentRecommendations || [];
const recommendation = recommendations.find(rec =>
rec.workload_name === workloadName &&
rec.namespace === namespace &&
rec.recommendation_type === recommendationType &&
rec.priority === priority
);
if (!recommendation) {
alert('Recommendation not found');
return;
}
// Confirm action
const confirmed = confirm(`Are you sure you want to apply this recommendation?\n\n${recommendation.title}\n\nThis will make changes to your cluster.`);
if (!confirmed) return;
// Apply recommendation
const response = await fetch('/api/v1/recommendations/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...recommendation,
dry_run: false
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Show success message
alert(`Recommendation applied successfully!\n\n${result.message}`);
// Refresh recommendations
loadSmartRecommendations();
} catch (error) {
console.error('Error applying recommendation:', error);
alert(`Failed to apply recommendation: ${error.message}`);
}
}
// Preview smart recommendation
async function previewSmartRecommendation(workloadName, namespace, recommendationType, priority) {
try {
// Find the recommendation data
const recommendations = window.currentRecommendations || [];
const recommendation = recommendations.find(rec =>
rec.workload_name === workloadName &&
rec.namespace === namespace &&
rec.recommendation_type === recommendationType &&
rec.priority === priority
);
if (!recommendation) {
alert('Recommendation not found');
return;
}
// Preview recommendation (dry run)
const response = await fetch('/api/v1/recommendations/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...recommendation,
dry_run: true
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Show preview modal
showRecommendationPreview(result);
} catch (error) {
console.error('Error previewing recommendation:', error);
alert(`Failed to preview recommendation: ${error.message}`);
}
}
// Show recommendation preview modal
function showRecommendationPreview(result) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'block';
modal.innerHTML = `
<div class="modal-content" style="width: 80%; max-width: 800px;">
<div class="modal-header">
<h3>Recommendation Preview</h3>
<span class="close" onclick="this.closest('.modal').remove()">&times;</span>
</div>
<div class="modal-body">
<div style="margin-bottom: 20px;">
<h4>${result.title}</h4>
<p style="color: var(--pf-global--Color--200);">${result.description}</p>
</div>
${result.implementation_steps && result.implementation_steps.length > 0 ? `
<div style="margin-bottom: 20px;">
<h5>Implementation Steps:</h5>
<ol style="margin: 8px 0 0 20px; color: var(--pf-global--Color--200);">
${result.implementation_steps.map(step => `<li>${step}</li>`).join('')}
</ol>
</div>
` : ''}
${result.kubectl_commands && result.kubectl_commands.length > 0 ? `
<div style="margin-bottom: 20px;">
<h5>Kubectl Commands:</h5>
<div style="background-color: var(--pf-global--BackgroundColor--200); padding: 12px; border-radius: 4px; font-family: monospace; font-size: 12px; color: var(--pf-global--Color--100);">
${result.kubectl_commands.map(cmd => `<div>${cmd}</div>`).join('')}
</div>
</div>
` : ''}
<div style="text-align: right; margin-top: 20px;">
<button class="openshift-button" onclick="this.closest('.modal').remove()">Close</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Close modal when clicking outside
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.remove();
}
});
}
// Show OpenShift Commands
function showOpenShiftCommands(workloadName, namespace) {
// Find the recommendation data
const recommendations = window.currentRecommendations || [];
const recommendation = recommendations.find(rec =>
rec.workload_name === workloadName && rec.namespace === namespace
);
if (!recommendation || !recommendation.kubectl_commands) {
alert('OpenShift commands not available for this recommendation');
return;
}
// Create modal with commands
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'block'; // Force display
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>OpenShift Commands - ${workloadName}</h3>
<span class="close" onclick="this.closest('.modal').remove()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Namespace:</strong> ${namespace}</p>
<p><strong>Commands:</strong></p>
<pre style="background-color: #1e1e1e; padding: 16px; border-radius: 4px; overflow-x: auto; color: var(--pf-global--Color--100);">${recommendation.kubectl_commands.join('\n')}</pre>
</div>
</div>
`;
document.body.appendChild(modal);
// Close modal when clicking outside
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.remove();
}
});
}
async function loadHistoricalAnalysis() {
try {
showLoading('historical-workloads-container');
// Get selected time range from PatternFly dropdown
const timeRangeText = document.getElementById('timeRangeText')?.textContent || 'Last 24 Hours';
let timeRange = '24h'; // default
switch(timeRangeText) {
case 'Last 1 Hour': timeRange = '1h'; break;
case 'Last 6 Hours': timeRange = '6h'; break;
case 'Last 24 Hours': timeRange = '24h'; break;
case 'Last 7 Days': timeRange = '7d'; break;
case 'Last 30 Days': timeRange = '30d'; break;
}
// Load historical data
const response = await fetch(`/api/v1/historical-analysis?time_range=${timeRange}`);
const data = await response.json();
updateHistoricalWorkloads(data);
} catch (error) {
console.error('Error loading historical analysis data:', error);
showError('historical-workloads-container', 'Failed to load historical data');
}
}
// Alias for the select onchange
function loadHistoricalWorkloads() {
loadHistoricalAnalysis();
}
// Time range dropdown functions
function toggleTimeRangeDropdown() {
const menu = document.getElementById('timeRangeMenu');
const toggle = document.getElementById('timeRangeToggle');
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
menu.style.display = isExpanded ? 'none' : 'block';
toggle.setAttribute('aria-expanded', !isExpanded);
}
function selectTimeRange(value, text) {
// Update selected item
document.querySelectorAll('#timeRangeMenu .pf-m-selected').forEach(item => {
item.classList.remove('pf-m-selected');
});
event.target.classList.add('pf-m-selected');
// Update display text
document.getElementById('timeRangeText').textContent = text;
// Close dropdown
document.getElementById('timeRangeMenu').style.display = 'none';
document.getElementById('timeRangeToggle').setAttribute('aria-expanded', 'false');
// Load data with new time range
loadHistoricalAnalysisWithTimeRange(value);
}
async function loadHistoricalAnalysisWithTimeRange(timeRange) {
try {
showLoading('historical-workloads-container');
// Load historical data
const response = await fetch(`/api/v1/historical-analysis?time_range=${timeRange}`);
const data = await response.json();
updateHistoricalWorkloads(data);
} catch (error) {
console.error('Error loading historical analysis data:', error);
showError('historical-workloads-container', 'Failed to load historical data');
}
}
function updateMetricsCards(data) {
document.getElementById('total-workloads').textContent = data.total_pods || 0;
document.getElementById('total-namespaces').textContent = data.total_namespaces || 0;
document.getElementById('critical-issues').textContent = data.total_errors || 0;
document.getElementById('total-warnings').textContent = data.total_warnings || 0;
// Update overcommit metrics
if (data.overcommit) {
document.getElementById('cpu-overcommit').textContent = `${data.overcommit.cpu_overcommit_percent || 0}%`;
document.getElementById('memory-overcommit').textContent = `${data.overcommit.memory_overcommit_percent || 0}%`;
document.getElementById('namespaces-in-overcommit').textContent = data.overcommit.namespaces_in_overcommit || 0;
document.getElementById('resource-utilization').textContent = `${(data.overcommit.resource_utilization || 0).toFixed(1)}%`;
// Store overcommit data for modal display
window.overcommitData = data.overcommit;
} else {
document.getElementById('cpu-overcommit').textContent = '0%';
document.getElementById('memory-overcommit').textContent = '0%';
document.getElementById('namespaces-in-overcommit').textContent = '0';
document.getElementById('resource-utilization').textContent = '0%';
}
}
function updateWorkloadsTable(data) {
const container = document.getElementById('workloads-table-container');
// Group validations by namespace
const namespaceGroups = {};
if (data.validations && data.validations.length > 0) {
data.validations.forEach(validation => {
const namespace = validation.namespace;
if (!namespaceGroups[namespace]) {
namespaceGroups[namespace] = {
namespace: namespace,
validations: [],
pods: new Set(),
severity_breakdown: { error: 0, warning: 0, info: 0 }
};
}
namespaceGroups[namespace].validations.push(validation);
namespaceGroups[namespace].pods.add(validation.pod_name);
// Count severity
if (validation.severity === 'error') {
namespaceGroups[namespace].severity_breakdown.error++;
} else if (validation.severity === 'warning') {
namespaceGroups[namespace].severity_breakdown.warning++;
} else if (validation.severity === 'info') {
namespaceGroups[namespace].severity_breakdown.info++;
}
});
}
const namespaces = Object.values(namespaceGroups);
if (namespaces.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--success-color--100);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Issues Found</h3>
<p style="margin: 0;">All workloads are properly configured</p>
</div>
`;
return;
}
// Create accordion HTML
const accordionHTML = `
<div class="workloads-accordion">
${namespaces.map((namespace, index) => `
<div class="workload-accordion-item">
<div class="workload-accordion-header" onclick="toggleWorkloadIssues(${index})" id="workload-header-${index}">
<div class="workload-accordion-title">
<i class="fas fa-chevron-right workload-accordion-icon" id="workload-icon-${index}"></i>
<strong style="color: var(--pf-global--Color--100);">${namespace.namespace}</strong>
</div>
<div class="workload-accordion-stats">
<span class="workload-stat">
<i class="fas fa-cube"></i>
${namespace.pods.size} pods
</span>
<span class="workload-stat">
<i class="fas fa-exclamation-triangle"></i>
${namespace.validations.length} issues
</span>
<span class="status-indicator ${getSeverityClass(namespace)}">
${getSeverityText(namespace)}
</span>
</div>
</div>
<div class="workload-accordion-content" id="workload-content-${index}" style="display: none;">
<div class="workload-issues-container" id="workload-issues-${index}">
<div class="loading-spinner">
<div class="spinner"></div>
Loading issues...
</div>
</div>
</div>
</div>
`).join('')}
</div>
`;
container.innerHTML = accordionHTML;
// Store namespace data globally for accordion functions
window.workloadsData = namespaces;
}
function updateHistoricalWorkloads(data) {
const container = document.getElementById('historical-workloads-container');
if (!data.workloads || data.workloads.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-chart-line" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--info-color--100);"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Historical Data</h3>
<p style="margin: 0;">No workloads available for analysis</p>
</div>
`;
return;
}
const tableHTML = `
<table class="table">
<thead>
<tr>
<th style="width: 30px;"></th>
<th>Workload</th>
<th>Namespace</th>
<th>Pods</th>
<th>CPU Usage</th>
<th>Memory Usage</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
${data.workloads.map((workload, index) => `
<tr class="workload-row" data-workload="${workload.name}" data-namespace="${workload.namespace}">
<td>
<button class="expand-button" onclick="toggleWorkloadDetails(${index})" id="expand-btn-${index}">
<i class="fas fa-chevron-right"></i>
</button>
</td>
<td>
<strong style="color: var(--pf-global--Color--100);">${workload.name}</strong>
</td>
<td>${workload.namespace}</td>
<td>${workload.pod_count || 0}</td>
<td>${workload.cpu_usage || 'N/A'}</td>
<td>${workload.memory_usage || 'N/A'}</td>
<td>${workload.last_updated ? new Date(workload.last_updated).toLocaleString() : 'N/A'}</td>
</tr>
<tr class="workload-details-row" id="details-${index}" style="display: none;">
<td colspan="7">
<div class="workload-details-container" id="details-content-${index}">
<div class="loading-spinner">
<div class="spinner"></div>
Loading workload details...
</div>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
function showWorkloadDetails(workloadName, namespace) {
// Update title
document.getElementById('workload-details-title').textContent = `${workloadName} - ${namespace}`;
// Load workload details
loadWorkloadDetails(workloadName, namespace);
// Show details container
document.getElementById('workload-details-container').classList.remove('section-hidden');
}
function toggleWorkloadDetails(index) {
const detailsRow = document.getElementById(`details-${index}`);
const expandBtn = document.getElementById(`expand-btn-${index}`);
const icon = expandBtn.querySelector('i');
if (detailsRow.style.display === 'none') {
// Expand the accordion
detailsRow.style.display = 'table-row';
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-down');
// Load data automatically when expanding
const workloadRow = expandBtn.closest('.workload-row');
const workloadName = workloadRow.dataset.workload;
const namespace = workloadRow.dataset.namespace;
if (workloadName && namespace) {
loadWorkloadDetails(workloadName, namespace, index);
}
} else {
// Collapse the accordion
detailsRow.style.display = 'none';
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-right');
}
}
async function loadWorkloadDetails(workloadName, namespace, index) {
const container = document.getElementById(`details-content-${index}`);
try {
// Get selected time range from PatternFly dropdown
const timeRangeText = document.getElementById('timeRangeText')?.textContent || 'Last 24 Hours';
let timeRange = '24h'; // default
switch(timeRangeText) {
case 'Last 1 Hour': timeRange = '1h'; break;
case 'Last 6 Hours': timeRange = '6h'; break;
case 'Last 24 Hours': timeRange = '24h'; break;
case 'Last 7 Days': timeRange = '7d'; break;
case 'Last 30 Days': timeRange = '30d'; break;
}
container.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
Loading workload details for ${timeRange}...
</div>
`;
const response = await fetch(`/api/v1/historical-analysis/${namespace}/${workloadName}?time_range=${timeRange}`);
const data = await response.json();
updateWorkloadDetailsAccordion(data, index, timeRange);
} catch (error) {
console.error('Error loading workload details:', error);
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--danger-color--100);">
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px;"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">Error</h3>
<p style="margin: 0;">Failed to load workload details</p>
</div>
`;
}
}
function updateWorkloadDetailsAccordion(data, index, timeRange = '24h') {
const container = document.getElementById(`details-content-${index}`);
// Create chart containers with unique IDs
const cpuChartId = `cpu-chart-${index}`;
const memoryChartId = `memory-chart-${index}`;
// Parse CPU and Memory data
const cpuData = data.cpu_data || {};
const memoryData = data.memory_data || {};
const recommendations = data.recommendations || [];
// Format time range for display
let timeRangeDisplay = '24h';
switch(timeRange) {
case '1h': timeRangeDisplay = '1 hour'; break;
case '6h': timeRangeDisplay = '6 hours'; break;
case '24h': timeRangeDisplay = '24 hours'; break;
case '7d': timeRangeDisplay = '7 days'; break;
case '30d': timeRangeDisplay = '30 days'; break;
}
container.innerHTML = `
<div style="padding: 24px; background-color: #1E1E1E; border-radius: 8px; margin: 16px 0;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px;">
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-microchip" style="margin-right: 8px; color: var(--pf-global--info-color--100);"></i>
CPU Usage (${timeRangeDisplay})
</h3>
</div>
<div style="padding: 20px;">
<div style="height: 350px; position: relative;">
<div id="${cpuChartId}" style="width: 100%; height: 300px; min-height: 300px;"></div>
</div>
${cpuData.data && cpuData.data.length > 0 ? `
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Current</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getCurrentValue(cpuData.data)} cores</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Average</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getAverageValue(cpuData.data)} cores</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Peak</div>
<div style="font-weight: bold; color: var(--pf-global--warning-color--100);">${getPeakValue(cpuData.data)} cores</div>
</div>
</div>
` : ''}
</div>
</div>
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-memory" style="margin-right: 8px; color: var(--pf-global--warning-color--100);"></i>
Memory Usage (${timeRangeDisplay})
</h3>
</div>
<div style="padding: 20px;">
<div style="height: 350px; position: relative;">
<div id="${memoryChartId}" style="width: 100%; height: 300px; min-height: 300px;"></div>
</div>
${memoryData.data && memoryData.data.length > 0 ? `
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Current</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getCurrentValue(memoryData.data)} MB</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Average</div>
<div style="font-weight: bold; color: var(--pf-global--Color--100);">${getAverageValue(memoryData.data)} MB</div>
</div>
<div style="text-align: center; padding: 8px; background-color: #2B2B2B; border-radius: 4px;">
<div style="font-size: 12px; color: var(--pf-global--Color--300);">Peak</div>
<div style="font-weight: bold; color: var(--pf-global--warning-color--100);">${getPeakValue(memoryData.data)} MB</div>
</div>
</div>
` : ''}
</div>
</div>
</div>
${recommendations.length > 0 ? `
<div class="openshift-card" style="margin-top: 24px;">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-lightbulb" style="margin-right: 8px; color: var(--pf-global--success-color--100);"></i>
Recommendations
</h3>
</div>
<div style="padding: 20px;">
${recommendations.map(rec => `
<div style="margin-bottom: 16px; padding: 16px; background-color: #2B2B2B; border-radius: 6px; border-left: 4px solid ${getSeverityColor(rec.severity)};">
<p style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);"><strong>${rec.type}:</strong> ${rec.message}</p>
<p style="margin: 0; color: var(--pf-global--Color--300);"><em>Recommendation:</em> ${rec.recommendation}</p>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
// Create charts after DOM is updated
setTimeout(() => {
createCPUChart(cpuChartId, cpuData, timeRange);
createMemoryChart(memoryChartId, memoryData, timeRange);
}, 100);
}
function getCurrentValue(data) {
if (!data || data.length === 0) return '0.000';
const lastValue = data[data.length - 1];
return lastValue ? lastValue.y.toFixed(3) : '0.000';
}
function getAverageValue(data) {
if (!data || data.length === 0) return '0.000';
const sum = data.reduce((acc, point) => acc + point.y, 0);
return (sum / data.length).toFixed(3);
}
function getPeakValue(data) {
if (!data || data.length === 0) return '0.000';
const max = Math.max(...data.map(point => point.y));
return max.toFixed(3);
}
function getSeverityColor(severity) {
switch(severity) {
case 'error': return 'var(--pf-global--danger-color--100)';
case 'warning': return 'var(--pf-global--warning-color--100)';
case 'info': return 'var(--pf-global--info-color--100)';
default: return 'var(--pf-global--Color--300)';
}
}
function showOvercommitDetails(type) {
if (!window.overcommitData) {
alert('Overcommit data not available');
return;
}
const data = window.overcommitData;
let title, content;
if (type === 'cpu') {
title = '🖥️ CPU Overcommit Details';
const cpuCapacity = data.cpu_capacity || 0;
const cpuRequests = data.cpu_requests || 0;
content = `
<div class="overcommit-details">
<h3>CPU Resource Analysis</h3>
<div class="metric-detail">
<strong>Capacity Total:</strong> ${cpuCapacity} cores
</div>
<div class="metric-detail">
<strong>Requests Total:</strong> ${cpuRequests} cores
</div>
<div class="metric-detail">
<strong>Overcommit:</strong> ${data.cpu_overcommit_percent}% (${cpuRequests} ÷ ${cpuCapacity} × 100)
</div>
<div class="metric-detail">
<strong>Available:</strong> ${(cpuCapacity - cpuRequests).toFixed(2)} cores
</div>
</div>
`;
} else if (type === 'memory') {
title = '💾 Memory Overcommit Details';
const memoryCapacity = data.memory_capacity || 0;
const memoryRequests = data.memory_requests || 0;
const memoryCapacityGB = (memoryCapacity / (1024**3)).toFixed(1);
const memoryRequestsGB = (memoryRequests / (1024**3)).toFixed(1);
content = `
<div class="overcommit-details">
<h3>Memory Resource Analysis</h3>
<div class="metric-detail">
<strong>Capacity Total:</strong> ${memoryCapacity.toLocaleString()} bytes (≈ ${memoryCapacityGB} GB)
</div>
<div class="metric-detail">
<strong>Requests Total:</strong> ${memoryRequests.toLocaleString()} bytes (≈ ${memoryRequestsGB} GB)
</div>
<div class="metric-detail">
<strong>Overcommit:</strong> ${data.memory_overcommit_percent}% (${memoryRequestsGB} ÷ ${memoryCapacityGB} × 100)
</div>
<div class="metric-detail">
<strong>Available:</strong> ${((memoryCapacity - memoryRequests) / (1024**3)).toFixed(1)} GB
</div>
</div>
`;
}
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>${title}</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
${content}
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
// Close modal functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = () => {
modal.remove();
};
modal.onclick = (e) => {
if (e.target === modal) {
modal.remove();
}
};
}
function showResourceUtilizationDetails() {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>📊 Resource Utilization Details</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="overcommit-details">
<h3>Resource Utilization Analysis</h3>
<div class="metric-detail">
<strong>Current Status:</strong>
<span style="color: #27ae60;">✅ Implemented with Prometheus Integration</span>
</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> ${(window.overcommitData?.resource_utilization || 0).toFixed(1)}% (real-time data from Prometheus)
</div>
<div class="metric-detail">
<strong>Data Source:</strong>
<span style="color: #3498db;">📊 Prometheus Metrics</span>
</div>
<div class="metric-detail">
<strong>Implementation Status:</strong>
<span style="color: #27ae60;">✅ Production Ready</span>
</div>
<div class="metric-detail">
<strong>Features:</strong>
<ul>
<li>Real-time CPU and memory utilization</li>
<li>Cluster-wide resource efficiency analysis</li>
<li>Automatic fallback to simulated data if Prometheus unavailable</li>
</ul>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
// Close modal functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = () => {
modal.remove();
};
modal.onclick = (e) => {
if (e.target === modal) {
modal.remove();
}
};
}
function createCPUChart(containerId, cpuData, timeRange = '24h') {
const container = document.getElementById(containerId);
if (!container) return;
const chartData = cpuData?.data || [];
// Clear container
container.innerHTML = '';
// Get container dimensions and ensure proper aspect ratio
const containerWidth = container.offsetWidth || 400;
const containerHeight = Math.max(container.offsetHeight || 200, 250); // Minimum height of 250px
// Create Victory chart using React.createElement
const chart = React.createElement(Victory.VictoryChart, {
width: containerWidth,
height: containerHeight,
theme: Victory.VictoryTheme.material,
scale: { x: 'time' },
padding: { top: 20, bottom: 60, left: 80, right: 40 },
domainPadding: { x: 0, y: 0 },
style: {
parent: {
background: '#1E1E1E',
width: '100%',
height: '100%'
}
}
}, [
React.createElement(Victory.VictoryAxis, {
key: 'y-axis',
dependentAxis: true,
style: {
axis: { stroke: '#404040' },
tickLabels: { fill: '#FFFFFF', fontSize: 12 },
grid: { stroke: '#404040' }
},
tickFormat: (t) => `${t.toFixed(3)} cores`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
scale: 'time',
tickCount: 12,
style: {
axis: { stroke: '#404040' },
tickLabels: { fill: '#FFFFFF', fontSize: 12 },
grid: { stroke: '#404040' }
},
tickFormat: (t) => {
const date = new Date(t);
// Check if we're dealing with days (7d or 30d)
if (timeRange === '7d' || timeRange === '30d') {
return date.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit'
});
} else {
return date.toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC'
});
}
}
}),
React.createElement(Victory.VictoryLine, {
key: 'cpu-line',
data: chartData,
style: {
data: {
stroke: '#0066CC',
strokeWidth: 2
}
},
animate: {
duration: 2000,
onLoad: { duration: 1000 }
}
}),
React.createElement(Victory.VictoryArea, {
key: 'cpu-area',
data: chartData,
style: {
data: {
fill: 'rgba(0, 102, 204, 0.1)',
fillOpacity: 0.3
}
}
})
]);
// Render the chart
ReactDOM.render(chart, container);
}
function createMemoryChart(containerId, memoryData, timeRange = '24h') {
const container = document.getElementById(containerId);
if (!container) return;
const chartData = memoryData?.data || [];
// Clear container
container.innerHTML = '';
// Get container dimensions and ensure proper aspect ratio
const containerWidth = container.offsetWidth || 400;
const containerHeight = Math.max(container.offsetHeight || 200, 250); // Minimum height of 250px
// Create Victory chart using React.createElement
const chart = React.createElement(Victory.VictoryChart, {
width: containerWidth,
height: containerHeight,
theme: Victory.VictoryTheme.material,
scale: { x: 'time' },
padding: { top: 20, bottom: 60, left: 80, right: 40 },
domainPadding: { x: 0, y: 0 },
style: {
parent: {
background: '#1E1E1E',
width: '100%',
height: '100%'
}
}
}, [
React.createElement(Victory.VictoryAxis, {
key: 'y-axis',
dependentAxis: true,
style: {
axis: { stroke: '#404040' },
tickLabels: { fill: '#FFFFFF', fontSize: 12 },
grid: { stroke: '#404040' }
},
tickFormat: (t) => `${t.toFixed(1)} MB`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
scale: 'time',
tickCount: 12,
style: {
axis: { stroke: '#404040' },
tickLabels: { fill: '#FFFFFF', fontSize: 12 },
grid: { stroke: '#404040' }
},
tickFormat: (t) => {
const date = new Date(t);
// Check if we're dealing with days (7d or 30d)
if (timeRange === '7d' || timeRange === '30d') {
return date.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit'
});
} else {
return date.toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC'
});
}
}
}),
React.createElement(Victory.VictoryLine, {
key: 'memory-line',
data: chartData,
style: {
data: {
stroke: '#CC0000',
strokeWidth: 2
}
},
animate: {
duration: 2000,
onLoad: { duration: 1000 }
}
}),
React.createElement(Victory.VictoryArea, {
key: 'memory-area',
data: chartData,
style: {
data: {
fill: 'rgba(204, 0, 0, 0.1)',
fillOpacity: 0.3
}
}
})
]);
// Render the chart
ReactDOM.render(chart, container);
}
function updateWorkloadDetails(data) {
const container = document.getElementById('workload-details-content');
container.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px;">
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">CPU Usage</h3>
</div>
<div style="padding: 20px; text-align: center; color: var(--pf-global--Color--300);">
<i class="fas fa-microchip" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--info-color--100);"></i>
<p>CPU usage data will be displayed here</p>
</div>
</div>
<div class="openshift-card">
<div class="card-header">
<h3 class="card-title">Memory Usage</h3>
</div>
<div style="padding: 20px; text-align: center; color: var(--pf-global--Color--300);">
<i class="fas fa-memory" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--warning-color--100);"></i>
<p>Memory usage data will be displayed here</p>
</div>
</div>
</div>
`;
}
function getSeverityClass(namespace) {
const breakdown = namespace.severity_breakdown || {};
if (breakdown.error > 0) return 'danger';
if (breakdown.warning > 0) return 'warning';
if (breakdown.info > 0) return 'info';
return 'success';
}
function getSeverityText(namespace) {
const breakdown = namespace.severity_breakdown || {};
if (breakdown.error > 0) return 'Error';
if (breakdown.warning > 0) return 'Warning';
if (breakdown.info > 0) return 'Info';
return 'Healthy';
}
function showLoading(containerId) {
const container = document.getElementById(containerId);
container.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
Loading...
</div>
`;
}
function showError(containerId, message) {
const container = document.getElementById(containerId);
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--danger-color--100);">
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px;"></i>
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">Error</h3>
<p style="margin: 0;">${message}</p>
</div>
`;
}
</script>
<!-- React and Victory.js for PatternFly charts -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/victory@36.6.12/dist/victory.min.js"></script>
</body>
</html>