Files
openshift-resource-governance/app/static/index.html
andersonid 0a0d3e1f43 feat: implement Smart Recommendations gallery with ServiceCard and BulkSelect
- Replace table layout with PatternFly ServiceCard gallery
- Add BulkSelect toolbar with select all/page/none options
- Implement individual VPA application per workload
- Add checkbox selection for each recommendation card
- Support bulk apply and preview operations
- Improve UX with card-based interface following PatternFly design
- Add responsive grid layout for recommendation cards
- Implement proper state management for selections
2025-10-02 19:16:22 -03:00

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