Files
openshift-resource-governance/app/static/index.html

5726 lines
238 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;
padding-top: 60px; /* Compensar header fixo */
}
/* 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);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.openshift-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.openshift-header-right {
display: flex;
align-items: center;
gap: 16px;
}
.refresh-button {
background: transparent;
border: 1px solid var(--pf-global--Color--400);
color: var(--pf-global--Color--200);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-button:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: var(--pf-global--Color--100);
color: var(--pf-global--Color--100);
}
.refresh-button:active {
transform: scale(0.98);
}
.refresh-button i {
font-size: 12px;
}
.refresh-button.loading i {
animation: spin 1s linear infinite;
}
.refresh-button.loading {
opacity: 0.7;
cursor: not-allowed;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.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);
}
.severity-breakdown {
font-size: 12px;
color: var(--pf-global--Color--300);
margin-top: 2px;
}
.critical-count {
color: var(--pf-global--danger-color--200);
font-weight: 600;
}
.error-count {
color: var(--pf-global--danger-color--100);
font-weight: 600;
}
.warning-count {
color: var(--pf-global--warning-color--100);
font-weight: 600;
}
.info-count {
color: var(--pf-global--info-color--100);
font-weight: 600;
}
.workload-accordion-content {
background-color: var(--pf-global--BackgroundColor--100);
border-top: 1px solid var(--pf-global--BorderColor--200);
}
.workload-issues-container {
padding: 20px;
}
/* Improved loading feedback */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
text-align: center;
}
.loading-spinner-enhanced {
width: 40px;
height: 40px;
border: 3px solid var(--pf-global--BorderColor--200);
border-top: 3px solid var(--pf-global--primary-color--100);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
.loading-text {
color: var(--pf-global--Color--200);
font-size: 16px;
margin-bottom: 8px;
}
.loading-subtext {
color: var(--pf-global--Color--300);
font-size: 14px;
}
.loading-progress {
width: 200px;
height: 4px;
background-color: var(--pf-global--BackgroundColor--300);
border-radius: 2px;
margin-top: 12px;
overflow: hidden;
}
.loading-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--pf-global--primary-color--100), var(--pf-global--info-color--100));
border-radius: 2px;
transition: width 0.3s ease;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* New Pod Card Styles */
.pod-card {
background-color: var(--pf-global--BackgroundColor--200);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 8px;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.pod-card-header {
background: linear-gradient(135deg, var(--pf-global--BackgroundColor--300) 0%, var(--pf-global--BackgroundColor--200) 100%);
padding: 16px 20px;
border-bottom: 1px solid var(--pf-global--BorderColor--200);
display: flex;
align-items: center;
justify-content: space-between;
}
.pod-name-section {
display: flex;
align-items: center;
gap: 12px;
}
.pod-name {
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.pod-severity-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.pod-severity-badge.warning {
background-color: var(--pf-global--warning-color--100);
color: var(--pf-global--Color--100);
}
.pod-severity-badge.error {
background-color: var(--pf-global--danger-color--100);
color: var(--pf-global--Color--100);
}
.pod-severity-badge.info {
background-color: var(--pf-global--info-color--100);
color: var(--pf-global--Color--100);
}
.pod-stats {
color: var(--pf-global--Color--200);
font-size: 14px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.issues-breakdown {
font-size: 12px;
color: var(--pf-global--Color--300);
}
.critical-count {
color: var(--pf-global--danger-color--200);
font-weight: 600;
}
.error-count {
color: var(--pf-global--danger-color--100);
font-weight: 600;
}
.warning-count {
color: var(--pf-global--warning-color--100);
font-weight: 600;
}
.info-count {
color: var(--pf-global--info-color--100);
font-weight: 600;
}
.pod-card-body {
padding: 20px;
}
.resource-section {
margin-bottom: 24px;
}
.resource-section:last-of-type {
margin-bottom: 0;
}
.resource-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--pf-global--BorderColor--300);
}
.resource-type-title {
font-size: 16px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.resource-count {
background-color: var(--pf-global--BackgroundColor--300);
color: var(--pf-global--Color--200);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.resource-issues {
display: flex;
flex-direction: column;
gap: 12px;
}
.resource-issue-item {
background-color: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 6px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.issue-details {
display: flex;
align-items: center;
gap: 16px;
}
.issue-ratio {
background-color: var(--pf-global--warning-color--100);
color: var(--pf-global--Color--100);
padding: 4px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
}
.issue-values {
color: var(--pf-global--Color--200);
font-size: 14px;
font-family: 'Courier New', monospace;
}
.issue-recommendation {
background-color: transparent;
color: var(--pf-global--Color--200);
padding: 12px 16px;
border-left: 4px solid var(--pf-global--success-color--100);
border-radius: 0;
font-size: 14px;
display: block;
margin-top: 8px;
}
/* CORES BASEADAS EM SEVERIDADE, NÃO TIPO DE RECURSO */
.issue-recommendation.error {
border-left-color: var(--pf-global--danger-color--100); /* VERMELHO para ERROR */
}
.issue-recommendation.warning {
border-left-color: var(--pf-global--warning-color--100); /* AMARELO para WARNING */
}
.issue-recommendation.info {
border-left-color: var(--pf-global--info-color--100); /* AZUL para INFO */
}
.issue-recommendation.critical {
border-left-color: var(--pf-global--danger-color--200); /* VERMELHO ESCURO para CRITICAL */
}
.other-issues-section {
margin-bottom: 24px;
}
.other-issues {
display: flex;
flex-direction: column;
gap: 8px;
}
.other-issue-item {
background-color: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 6px;
padding: 12px;
border-left: 4px solid var(--pf-global--Color--400); /* Default gray */
}
/* CORES BASEADAS EM SEVERIDADE PARA OTHER ISSUES */
.other-issue-item.error {
border-left-color: var(--pf-global--danger-color--100); /* VERMELHO para ERROR */
}
.other-issue-item.warning {
border-left-color: var(--pf-global--warning-color--100); /* AMARELO para WARNING */
}
.other-issue-item.info {
border-left-color: var(--pf-global--info-color--100); /* AZUL para INFO */
}
.other-issue-item.critical {
border-left-color: var(--pf-global--danger-color--200); /* VERMELHO ESCURO para CRITICAL */
}
.issue-type {
font-weight: 600;
color: var(--pf-global--Color--100);
font-size: 14px;
display: block;
margin-bottom: 4px;
}
.issue-message {
color: var(--pf-global--Color--200);
font-size: 14px;
margin: 0;
}
.pod-actions {
display: flex;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--pf-global--BorderColor--200);
}
.action-btn {
padding: 10px 16px;
border-radius: 6px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
text-decoration: none;
}
.apply-fix-btn {
background-color: var(--pf-global--primary-color--100);
color: var(--pf-global--Color--100);
}
.apply-fix-btn:hover {
background-color: var(--pf-global--primary-color--200);
transform: translateY(-1px);
}
.view-yaml-btn {
background-color: transparent;
color: var(--pf-global--Color--200);
border: 1px solid var(--pf-global--BorderColor--200);
}
.view-yaml-btn:hover {
background-color: var(--pf-global--BackgroundColor--300);
color: var(--pf-global--Color--100);
border-color: var(--pf-global--BorderColor--100);
}
.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">
<button class="refresh-button" id="refreshButton" title="Refresh cluster analysis">
<i class="fas fa-sync-alt"></i>
<span>Refresh</span>
</button>
<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>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="storage-analysis">
<i class="fas fa-hdd sidebar-nav-icon"></i>
<span>Storage Analysis</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>
<!-- Storage Analysis Section -->
<section id="storage-analysis-section" class="section-hidden">
<div class="page-header">
<h1 class="page-title">Storage Analysis</h1>
<p class="page-description">Analyze storage usage, consumption patterns, and available capacity across workloads</p>
</div>
<!-- Storage Overview Cards -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">
<i class="fas fa-hdd"></i>
</div>
<div class="metric-content">
<div class="metric-value" id="total-pvcs">-</div>
<div class="metric-label">Total PVCs</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">
<i class="fas fa-database"></i>
</div>
<div class="metric-content">
<div class="metric-value" id="total-storage-used">-</div>
<div class="metric-label">Storage Used</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">
<i class="fas fa-chart-pie"></i>
</div>
<div class="metric-content">
<div class="metric-value" id="storage-utilization">-</div>
<div class="metric-label">Utilization</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="metric-content">
<div class="metric-value" id="storage-warnings">-</div>
<div class="metric-label">Storage Warnings</div>
</div>
</div>
</div>
<!-- Storage Analytics Charts -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Storage Analytics</h2>
<button class="openshift-button" id="refresh-storage">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<div class="card-content">
<div class="charts-grid">
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-chart-line"></i>
Storage Usage Trend (24h)
</h3>
<div id="storage-trend-chart" class="chart-placeholder">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading storage trend...
</div>
</div>
</div>
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-chart-pie"></i>
Storage by Namespace
</h3>
<div id="storage-namespace-chart" class="chart-placeholder">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading namespace distribution...
</div>
</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-list"></i>
Top 10 Workloads by Storage Usage
</h3>
<div id="top-storage-workloads-chart" class="chart-placeholder">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading top workloads...
</div>
</div>
</div>
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-chart-bar"></i>
Storage Classes Distribution
</h3>
<div id="storage-classes-chart" class="chart-placeholder">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading storage classes...
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Storage Details Table -->
<div class="openshift-card">
<div class="card-header">
<h2 class="card-title">Storage Details by Namespace</h2>
<div class="card-actions">
<div class="search-box">
<input type="text" id="storage-search" placeholder="Search namespaces..." class="search-input">
<i class="fas fa-search search-icon"></i>
</div>
</div>
</div>
<div class="table-content" id="storage-details-container">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
Loading storage details...
</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';
// Cache system
let clusterCache = {
data: null,
timestamp: null,
ttl: 5 * 60 * 1000, // 5 minutes TTL
isAnalyzing: false
};
// Cache utility functions
function isCacheValid() {
if (!clusterCache.data || !clusterCache.timestamp) {
return false;
}
const now = Date.now();
return (now - clusterCache.timestamp) < clusterCache.ttl;
}
function setCacheData(data) {
clusterCache.data = data;
clusterCache.timestamp = Date.now();
clusterCache.isAnalyzing = false;
}
function clearCache() {
clusterCache.data = null;
clusterCache.timestamp = null;
clusterCache.isAnalyzing = false;
}
function getCachedData() {
return clusterCache.data;
}
function setRefreshButtonLoading(loading) {
const refreshButton = document.getElementById('refreshButton');
if (refreshButton) {
if (loading) {
refreshButton.classList.add('loading');
refreshButton.disabled = true;
} else {
refreshButton.classList.remove('loading');
refreshButton.disabled = false;
}
}
}
let loadingState = {
dashboard: false,
charts: {
resourceUtilization: false,
namespaceDistribution: false,
issuesTimeline: false,
topWorkloads: false,
overcommitByNamespace: false
}
};
// Smart loading system
let loadingProgress = {
lastActivity: Date.now(),
timeoutId: null,
isActive: false,
progressSteps: 0,
totalSteps: 3
};
// Storage Analysis Functions
async function loadStorageAnalysis() {
try {
console.log('Loading storage analysis...');
showLoading('storage-details-container');
// Load storage data from API
const response = await fetch('/api/v1/storage/analysis');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Update storage metrics cards
updateStorageMetrics(data);
// Load storage charts
await loadStorageCharts(data);
// Update storage details table
updateStorageDetails(data);
console.log('Storage analysis loaded successfully');
} catch (error) {
console.error('Error loading storage analysis:', error);
showError('storage-details-container', `Error loading storage analysis: ${error.message}`);
}
}
function updateStorageMetrics(data) {
// Update storage overview cards
document.getElementById('total-pvcs').textContent = data.total_pvcs || '0';
document.getElementById('total-storage-used').textContent = formatBytes(data.total_storage_used || 0);
document.getElementById('storage-utilization').textContent = `${data.storage_utilization_percent || 0}%`;
document.getElementById('storage-warnings').textContent = data.storage_warnings || '0';
}
async function loadStorageCharts(data) {
try {
// Storage Usage Trend Chart
await loadStorageTrendChart(data);
// Storage by Namespace Chart
await loadStorageNamespaceChart(data);
// Top Storage Workloads Chart
await loadTopStorageWorkloadsChart(data);
// Storage Classes Chart
await loadStorageClassesChart(data);
} catch (error) {
console.error('Error loading storage charts:', error);
}
}
async function loadStorageTrendChart(data) {
const container = document.getElementById('storage-trend-chart');
// Simulate trend data (in real implementation, this would come from Prometheus)
const trendData = generateStorageTrendData();
container.innerHTML = `
<div style="width: 100%; height: 300px;">
<canvas id="storage-trend-canvas"></canvas>
</div>
`;
// Create line chart using Chart.js (if available) or Victory.js
setTimeout(() => {
if (typeof Victory !== 'undefined') {
const chart = new Victory.Line({
data: trendData,
width: 400,
height: 300,
style: {
data: { stroke: "#0066cc" }
}
});
chart.render(container.querySelector('#storage-trend-canvas'));
} else {
// Fallback to simple visualization
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;"></i>
<p>Storage trend visualization</p>
<p style="font-size: 14px; color: var(--pf-global--Color--400);">
Chart library not available
</p>
</div>
`;
}
}, 100);
}
async function loadStorageNamespaceChart(data) {
const container = document.getElementById('storage-namespace-chart');
// Create horizontal bar chart for namespace storage distribution
const namespaceData = data.namespace_storage || [];
const totalStorage = namespaceData.reduce((sum, ns) => sum + (ns.storage_used || 0), 0);
if (namespaceData.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-database" style="font-size: 48px; margin-bottom: 16px;"></i>
<p>No storage data available</p>
</div>
`;
return;
}
// Sort by storage usage and take top 10
const sortedData = namespaceData
.sort((a, b) => (b.storage_used || 0) - (a.storage_used || 0))
.slice(0, 10);
const chartHTML = sortedData.map((ns, index) => {
const percentage = totalStorage > 0 ? ((ns.storage_used || 0) / totalStorage * 100) : 0;
const width = Math.max(percentage * 3, 2); // Minimum 2px width
return `
<div class="namespace-storage-item" style="margin-bottom: 12px;">
<div class="namespace-name" style="font-size: 14px; margin-bottom: 4px; color: var(--pf-global--Color--100);">
${ns.namespace}
</div>
<div class="storage-bar-container" style="background-color: var(--pf-global--BackgroundColor--300); height: 20px; border-radius: 10px; overflow: hidden; position: relative;">
<div class="storage-bar" style="
background: linear-gradient(90deg, var(--pf-global--primary-color--100), var(--pf-global--info-color--100));
height: 100%;
width: ${width}px;
border-radius: 10px;
transition: width 0.3s ease;
"></div>
<div class="storage-percentage" style="
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: var(--pf-global--Color--100);
font-weight: 600;
">${percentage.toFixed(1)}%</div>
</div>
<div class="storage-details" style="font-size: 12px; color: var(--pf-global--Color--300); margin-top: 2px;">
${formatBytes(ns.storage_used || 0)} used
</div>
</div>
`;
}).join('');
container.innerHTML = `
<div style="padding: 20px;">
${chartHTML}
</div>
`;
}
async function loadTopStorageWorkloadsChart(data) {
const container = document.getElementById('top-storage-workloads-chart');
const workloadData = data.top_storage_workloads || [];
if (workloadData.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-list" style="font-size: 48px; margin-bottom: 16px;"></i>
<p>No workload storage data available</p>
</div>
`;
return;
}
const chartHTML = workloadData.map((workload, index) => {
const percentage = data.max_storage > 0 ? (workload.storage_used / data.max_storage * 100) : 0;
const width = Math.max(percentage * 2, 2);
return `
<div class="workload-storage-item" style="margin-bottom: 16px; padding: 12px; background-color: var(--pf-global--BackgroundColor--200); border-radius: 6px;">
<div class="workload-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div class="workload-name" style="font-weight: 600; color: var(--pf-global--Color--100);">
${workload.name}
</div>
<div class="workload-storage" style="font-size: 14px; color: var(--pf-global--Color--200);">
${formatBytes(workload.storage_used)}
</div>
</div>
<div class="storage-bar-container" style="background-color: var(--pf-global--BackgroundColor--300); height: 16px; border-radius: 8px; overflow: hidden; position: relative;">
<div class="storage-bar" style="
background: linear-gradient(90deg, var(--pf-global--success-color--100), var(--pf-global--warning-color--100));
height: 100%;
width: ${width}px;
border-radius: 8px;
transition: width 0.3s ease;
"></div>
</div>
<div class="workload-details" style="font-size: 12px; color: var(--pf-global--Color--300); margin-top: 4px;">
Namespace: ${workload.namespace} • PVCs: ${workload.pvc_count}
</div>
</div>
`;
}).join('');
container.innerHTML = `
<div style="padding: 20px; max-height: 400px; overflow-y: auto;">
${chartHTML}
</div>
`;
}
async function loadStorageClassesChart(data) {
const container = document.getElementById('storage-classes-chart');
const storageClassData = data.storage_classes || [];
if (storageClassData.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-chart-bar" style="font-size: 48px; margin-bottom: 16px;"></i>
<p>No storage class data available</p>
</div>
`;
return;
}
const chartHTML = storageClassData.map((sc, index) => {
const percentage = data.total_pvcs > 0 ? (sc.pvc_count / data.total_pvcs * 100) : 0;
const width = Math.max(percentage * 2, 2);
return `
<div class="storage-class-item" style="margin-bottom: 12px;">
<div class="storage-class-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<div class="storage-class-name" style="font-weight: 600; color: var(--pf-global--Color--100);">
${sc.name}
</div>
<div class="storage-class-count" style="font-size: 14px; color: var(--pf-global--Color--200);">
${sc.pvc_count} PVCs
</div>
</div>
<div class="storage-bar-container" style="background-color: var(--pf-global--BackgroundColor--300); height: 18px; border-radius: 9px; overflow: hidden; position: relative;">
<div class="storage-bar" style="
background: linear-gradient(90deg, var(--pf-global--info-color--100), var(--pf-global--primary-color--100));
height: 100%;
width: ${width}px;
border-radius: 9px;
transition: width 0.3s ease;
"></div>
<div class="storage-percentage" style="
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: var(--pf-global--Color--100);
font-weight: 600;
">${percentage.toFixed(1)}%</div>
</div>
</div>
`;
}).join('');
container.innerHTML = `
<div style="padding: 20px;">
${chartHTML}
</div>
`;
}
function updateStorageDetails(data) {
const container = document.getElementById('storage-details-container');
const namespaceData = data.namespace_storage || [];
if (namespaceData.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
<i class="fas fa-database" style="font-size: 48px; margin-bottom: 16px;"></i>
<p>No storage data available</p>
</div>
`;
return;
}
// Sort by storage usage
const sortedData = namespaceData.sort((a, b) => (b.storage_used || 0) - (a.storage_used || 0));
const tableHTML = `
<div class="openshift-table">
<table>
<thead>
<tr>
<th>Namespace</th>
<th>Storage Used</th>
<th>PVCs</th>
<th>Storage Classes</th>
<th>Utilization</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${sortedData.map(ns => `
<tr>
<td>
<div class="namespace-info">
<strong>${ns.namespace}</strong>
</div>
</td>
<td>
<div class="storage-info">
${formatBytes(ns.storage_used || 0)}
</div>
</td>
<td>
<span class="pvc-count">${ns.pvc_count || 0}</span>
</td>
<td>
<div class="storage-classes">
${(ns.storage_classes || []).map(sc =>
`<span class="storage-class-tag">${sc}</span>`
).join(' ')}
</div>
</td>
<td>
<div class="utilization-bar">
<div class="utilization-fill" style="width: ${ns.utilization_percent || 0}%"></div>
<span class="utilization-text">${ns.utilization_percent || 0}%</span>
</div>
</td>
<td>
<span class="status-badge ${getStorageStatusClass(ns)}">
${getStorageStatusText(ns)}
</span>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = tableHTML;
}
function generateStorageTrendData() {
// Generate mock trend data for 24 hours
const data = [];
const now = new Date();
for (let i = 23; i >= 0; i--) {
const time = new Date(now.getTime() - (i * 60 * 60 * 1000));
const usage = 100 + Math.random() * 50; // Mock usage between 100-150GB
data.push({
x: time.toISOString(),
y: usage
});
}
return data;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getStorageStatusClass(ns) {
const utilization = ns.utilization_percent || 0;
if (utilization >= 90) return 'danger';
if (utilization >= 75) return 'warning';
if (utilization >= 50) return 'info';
return 'success';
}
function getStorageStatusText(ns) {
const utilization = ns.utilization_percent || 0;
if (utilization >= 90) return 'Critical';
if (utilization >= 75) return 'Warning';
if (utilization >= 50) return 'Info';
return 'Healthy';
}
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
function initializeApp() {
// Setup navigation
setupNavigation();
// Setup refresh button
setupRefreshButton();
// Load initial data
loadWorkloadScanner();
}
function setupRefreshButton() {
const refreshButton = document.getElementById('refreshButton');
if (refreshButton) {
refreshButton.addEventListener('click', function(e) {
e.preventDefault();
console.log('Manual refresh triggered');
clearCache();
loadWorkloadScanner(true);
});
}
}
// Loading Functions
function showFullscreenLoading(message = 'Analyzing Cluster Resources', submessage = 'Please wait while we analyze your cluster resources...') {
// Create fullscreen loading modal
const loadingModal = document.createElement('div');
loadingModal.id = 'fullscreen-loading';
loadingModal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(21, 21, 21, 0.95);
backdrop-filter: blur(10px);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--pf-global--Color--100);
font-family: var(--pf-global--FontFamily--sans-serif);
`;
loadingModal.innerHTML = `
<div style="text-align: center; max-width: 600px; padding: 2rem;">
<div style="margin-bottom: 2rem;">
<i class="fas fa-spinner fa-spin" style="font-size: 4rem; color: var(--pf-global--primary-color--100);"></i>
</div>
<h1 style="font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--pf-global--Color--100); font-weight: 600;">
${message}
</h1>
<p style="font-size: 1.2rem; margin: 0 0 2rem 0; color: var(--pf-global--Color--300);">
${submessage}
</p>
<div style="background: var(--pf-global--BackgroundColor--300); border-radius: 8px; padding: 1rem; margin: 1rem 0;">
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 0.5rem;">
<i class="fas fa-server" style="margin-right: 0.5rem; color: var(--pf-global--primary-color--100);"></i>
<span>Connecting to OpenShift API...</span>
</div>
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 0.5rem;">
<i class="fas fa-chart-line" style="margin-right: 0.5rem; color: var(--pf-global--primary-color--100);"></i>
<span>Querying Prometheus metrics...</span>
</div>
<div style="display: flex; align-items: center; justify-content: center;">
<i class="fas fa-cogs" style="margin-right: 0.5rem; color: var(--pf-global--primary-color--100);"></i>
<span>Analyzing resource usage patterns...</span>
</div>
</div>
<div style="margin-top: 2rem; display: flex; justify-content: center;">
<div style="width: 300px; height: 4px; background: var(--pf-global--BackgroundColor--300); border-radius: 2px; overflow: hidden;">
<div id="loading-progress" style="width: 0%; height: 100%; background: linear-gradient(90deg, var(--pf-global--primary-color--100), var(--pf-global--info-color--100)); transition: width 0.3s ease;"></div>
</div>
</div>
</div>
`;
document.body.appendChild(loadingModal);
// Animate progress bar
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 90) progress = 90;
const progressElement = document.getElementById('loading-progress');
if (progressElement) {
progressElement.style.width = progress + '%';
}
}, 200);
return { modal: loadingModal, progressInterval };
}
function hideFullscreenLoading() {
const loadingModal = document.getElementById('fullscreen-loading');
if (loadingModal) {
loadingModal.remove();
}
}
function showChartLoading(containerId, message = 'Loading chart...') {
const container = document.getElementById(containerId);
if (!container) return;
const loadingHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 200px; color: var(--pf-global--Color--300);">
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 1rem; color: var(--pf-global--primary-color--100);"></i>
<p style="margin: 0; font-size: 0.9rem;">${message}</p>
</div>
`;
container.innerHTML = loadingHTML;
}
function hideLoading(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
// Remove loading indicators
const loadingElements = container.querySelectorAll('.fa-spinner, .pf-c-empty-state');
loadingElements.forEach(el => el.remove());
}
function updateLoadingProgress(loaded, total) {
const progress = Math.round((loaded / total) * 100);
const progressBar = document.getElementById('loading-progress');
if (progressBar) {
progressBar.style.width = progress + '%';
}
console.log(`Loading progress: ${progress}% (${loaded}/${total})`);
}
// Smart loading system functions
function resetLoadingTimeout() {
loadingProgress.lastActivity = Date.now();
if (loadingProgress.timeoutId) {
clearTimeout(loadingProgress.timeoutId);
}
// Set timeout only if no activity for 30 seconds
loadingProgress.timeoutId = setTimeout(() => {
if (loadingProgress.isActive) {
console.log('Loading timeout - no activity for 30 seconds');
hideFullscreenLoading();
console.error('Request timeout - API stopped responding');
}
}, 30000);
}
function updateSmartProgress(step, message = '') {
loadingProgress.progressSteps = step;
loadingProgress.lastActivity = Date.now();
// Update progress bar
const progress = Math.round((step / loadingProgress.totalSteps) * 100);
const progressBar = document.getElementById('loading-progress');
if (progressBar) {
progressBar.style.width = progress + '%';
}
// Update status message if provided
if (message) {
console.log(`Loading progress: ${progress}% - ${message}`);
}
// Reset timeout since we have activity
resetLoadingTimeout();
}
function startSmartLoading() {
loadingProgress.isActive = true;
loadingProgress.progressSteps = 0;
loadingProgress.lastActivity = Date.now();
resetLoadingTimeout();
}
function stopSmartLoading() {
loadingProgress.isActive = false;
if (loadingProgress.timeoutId) {
clearTimeout(loadingProgress.timeoutId);
loadingProgress.timeoutId = null;
}
}
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-storage').addEventListener('click', loadStorageAnalysis);
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(false); // Use cache if available
} else if (section === 'requests-limits') {
loadRequestsLimits();
} else if (section === 'storage-analysis') {
loadStorageAnalysis();
} else if (section === 'vpa-management') {
loadVPAManagement();
} else if (section === 'historical-analysis') {
loadHistoricalAnalysis();
} else if (section === 'settings') {
loadSettings();
}
}
async function loadBatchValidations() {
try {
console.log('Loading validations using batch processing...');
const response = await fetch('/api/v1/batch/validations?page=1&page_size=50');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(`Loaded ${data.validations.length} validations using batch processing`);
// Update validations table if it exists
if (typeof updateValidationsTable === 'function') {
updateValidationsTable(data.validations);
}
return data;
} catch (error) {
console.error('Error loading batch validations:', error);
throw error;
}
}
async function loadWorkloadScanner(forceRefresh = false) {
try {
// Check cache first (unless force refresh)
if (!forceRefresh && isCacheValid() && !clusterCache.isAnalyzing) {
console.log('Using cached cluster data');
const cachedData = getCachedData();
updateMetricsCards(cachedData);
currentData = { cluster: cachedData };
await loadDashboardCharts();
return;
}
// If already analyzing, don't start another analysis
if (clusterCache.isAnalyzing) {
console.log('Analysis already in progress, waiting...');
return;
}
// Mark as analyzing
clusterCache.isAnalyzing = true;
setRefreshButtonLoading(true);
// Show fullscreen loading modal
showFullscreenLoading(
'Analyzing Cluster Resources',
'Starting background analysis pipeline... This may take up to 2 minutes for large clusters.'
);
// Start smart loading system
startSmartLoading();
// Step 1: Start background cluster analysis
updateSmartProgress(0, 'Starting background cluster analysis...');
const analysisResponse = await fetch('/api/v1/tasks/cluster/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!analysisResponse.ok) {
throw new Error(`HTTP error! status: ${analysisResponse.status}`);
}
const analysisData = await analysisResponse.json();
const taskId = analysisData.task_id;
updateSmartProgress(1, 'Background analysis started, monitoring progress...');
// Step 2: Monitor task progress
let taskCompleted = false;
let attempts = 0;
const maxAttempts = 120; // 2 minutes max
while (!taskCompleted && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
attempts++;
try {
const statusResponse = await fetch(`/api/v1/tasks/${taskId}/status`);
if (!statusResponse.ok) {
throw new Error(`HTTP error! status: ${statusResponse.status}`);
}
const statusData = await statusResponse.json();
if (statusData.state === 'PROGRESS') {
const progress = Math.round((statusData.current / statusData.total) * 100);
updateSmartProgress(progress, statusData.status);
} else if (statusData.state === 'SUCCESS') {
updateSmartProgress(100, 'Analysis completed successfully!');
// Save to cache
setCacheData(statusData.result);
// Update metrics cards with results
updateMetricsCards(statusData.result);
// Load dashboard charts
updateSmartProgress(100, 'Loading dashboard charts...');
await loadDashboardCharts();
currentData = { cluster: statusData.result };
taskCompleted = true;
} else if (statusData.state === 'FAILURE') {
throw new Error(`Analysis failed: ${statusData.error}`);
}
} catch (error) {
console.warn('Error checking task status:', error);
if (attempts >= maxAttempts) {
throw new Error('Analysis timeout - please try again');
}
}
}
if (!taskCompleted) {
throw new Error('Analysis timeout - please try again');
}
// Hide loading modal
hideFullscreenLoading();
stopSmartLoading();
setRefreshButtonLoading(false);
} catch (error) {
console.error('Error in batch processing workflow:', error);
hideFullscreenLoading();
stopSmartLoading();
// Clear analyzing flag on error
clusterCache.isAnalyzing = false;
setRefreshButtonLoading(false);
if (error.name === 'AbortError') {
console.error('Request timeout - API stopped responding');
} else {
console.error('Failed to load cluster data: ' + error.message);
}
}
}
async function loadRequestsLimits() {
try {
showLoading('workloads-table-container');
// Load validations (load all pages to ensure we get all namespaces)
const validationsResponse = await fetch('/api/v1/validations?page=1&page_size=10000');
const validationsData = await validationsResponse.json();
hideLoading('workloads-table-container');
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 {
// Use the same data source as updateWorkloadsTable to ensure consistency
const response = await fetch('/api/v1/validations?page=1&page_size=10000');
const data = await response.json();
// Group validations by namespace (same logic as updateWorkloadsTable)
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: []
};
}
namespaceGroups[namespace].validations.push(validation);
});
}
// Store the data for each namespace
window.workloadDetails = {};
Object.values(namespaceGroups).forEach(namespace => {
window.workloadDetails[namespace.namespace] = {
validations: namespace.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 enhanced loading with progress
showEnhancedLoading(container, namespace);
// Load data asynchronously and update UI when ready
loadNamespaceDetails(namespace.namespace, index);
}
async function loadNamespaceDetails(namespaceName, index) {
try {
console.log(`Loading details for namespace: ${namespaceName}`);
// Fetch validations for this namespace
const response = await fetch(`/api/v1/validations?namespace=${namespaceName}&page=1&page_size=100`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Store the data
if (!window.workloadDetails) {
window.workloadDetails = {};
}
window.workloadDetails[namespaceName] = data;
// Update the UI
const container = document.getElementById(`workload-issues-${index}`);
const namespace = window.workloadsData[index];
if (container && namespace) {
displayWorkloadIssues(container, data, namespace);
}
console.log(`Details loaded for namespace: ${namespaceName}`);
} catch (error) {
console.error(`Error loading details for ${namespaceName}:`, error);
// Store error and update UI
if (!window.workloadDetails) {
window.workloadDetails = {};
}
window.workloadDetails[namespaceName] = { error: error.message };
const container = document.getElementById(`workload-issues-${index}`);
if (container) {
container.innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-circle"></i>
${error.message}
</div>
`;
}
}
}
function showEnhancedLoading(container, namespace) {
const totalPods = namespace.pods ? namespace.pods.size : 0;
const totalIssues = namespace.validations ? namespace.validations.length : 0;
container.innerHTML = `
<div class="loading-container">
<div class="loading-spinner-enhanced"></div>
<div class="loading-text">Loading workload issues...</div>
<div class="loading-subtext">Analyzing ${namespace.namespace} namespace (${totalPods} pods, ${totalIssues} issues)</div>
<div class="loading-progress">
<div class="loading-progress-bar" style="width: 60%"></div>
</div>
</div>
`;
// Simulate progress update
let progress = 60;
const progressInterval = setInterval(() => {
progress += Math.random() * 10;
if (progress > 90) progress = 90;
const progressBar = container.querySelector('.loading-progress-bar');
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
}, 500);
// Store interval for cleanup
container._progressInterval = progressInterval;
}
function displayWorkloadIssues(container, data, namespace) {
// Clean up progress interval if exists
if (container._progressInterval) {
clearInterval(container._progressInterval);
container._progressInterval = null;
}
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;
}
// Group validations by pod for better organization
const podGroups = groupValidationsByPod(data.validations);
const issuesHTML = `
<div class="workload-issues-list">
${Object.values(podGroups).map(pod => createPodCard(pod)).join('')}
</div>
`;
container.innerHTML = issuesHTML;
}
function groupValidationsByPod(validations) {
const groups = {};
validations.forEach(validation => {
const podName = validation.pod_name;
if (!groups[podName]) {
groups[podName] = {
pod_name: podName,
validations: [],
severity: 'info',
cpuIssues: [],
memoryIssues: [],
otherIssues: []
};
}
groups[podName].validations.push(validation);
// Categorize issues by validation type and message content
if (validation.validation_type === 'invalid_ratio' && validation.message && validation.message.includes('CPU')) {
groups[podName].cpuIssues.push(validation);
} else if (validation.validation_type === 'invalid_ratio' && validation.message && validation.message.includes('Memory')) {
groups[podName].memoryIssues.push(validation);
} else {
groups[podName].otherIssues.push(validation);
}
// Set highest severity
if (validation.severity === 'error' ||
(validation.severity === 'warning' && groups[podName].severity !== 'error')) {
groups[podName].severity = validation.severity;
}
});
return groups;
}
function getIssuesBreakdown(validations) {
const counts = { error: 0, warning: 0, info: 0, critical: 0 };
validations.forEach(v => {
if (counts[v.severity] !== undefined) {
counts[v.severity]++;
}
});
const parts = [];
if (counts.critical > 0) parts.push(`<span class="critical-count">${counts.critical} critical</span>`);
if (counts.error > 0) parts.push(`<span class="error-count">${counts.error} errors</span>`);
if (counts.warning > 0) parts.push(`<span class="warning-count">${counts.warning} warnings</span>`);
if (counts.info > 0) parts.push(`<span class="info-count">${counts.info} info</span>`);
return parts.join(' • ');
}
function createPodCard(pod) {
return `
<div class="pod-card">
<div class="pod-card-header">
<div class="pod-name-section">
<h3 class="pod-name">${pod.pod_name}</h3>
<span class="pod-severity-badge ${pod.severity}">
${pod.severity.toUpperCase()}
</span>
</div>
<div class="pod-stats">
<span class="pod-issues-count">${pod.validations.length} issues</span>
<div class="issues-breakdown">
${getIssuesBreakdown(pod.validations)}
</div>
</div>
</div>
<div class="pod-card-body">
${createResourceSection('CPU', pod.cpuIssues)}
${createResourceSection('Memory', pod.memoryIssues)}
${createOtherIssuesSection(pod.otherIssues)}
<div class="pod-actions">
<button class="action-btn apply-fix-btn" onclick="applyResourceFix('${pod.pod_name}')">
<i class="fas fa-wrench"></i>
Apply Fix
</button>
<button class="action-btn view-yaml-btn" onclick="viewYamlPatch('${pod.pod_name}')">
<i class="fas fa-code"></i>
View YAML
</button>
</div>
</div>
</div>
`;
}
function createResourceSection(resourceType, issues) {
if (!issues || issues.length === 0) return '';
const recommendations = issues.map(issue => generateSpecificRecommendation(issue, resourceType)).join('');
return `
<div class="resource-section">
<div class="resource-section-header">
<h4 class="resource-type-title">${resourceType} Issues</h4>
<span class="resource-count">${issues.length} issue${issues.length > 1 ? 's' : ''}</span>
</div>
<div class="resource-issues">
${issues.map(issue => `
<div class="resource-issue-item">
<div class="issue-details">
<span class="issue-ratio">${extractRatio(issue.message)}</span>
<span class="issue-values">${extractValues(issue.message)}</span>
</div>
<div class="issue-recommendation ${issue.severity}">
${generateSpecificRecommendation(issue, resourceType)}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
function createOtherIssuesSection(issues) {
if (!issues || issues.length === 0) return '';
return `
<div class="other-issues-section">
<div class="resource-section-header">
<h4 class="resource-type-title">Other Issues</h4>
<span class="resource-count">${issues.length} issue${issues.length > 1 ? 's' : ''}</span>
</div>
<div class="other-issues">
${issues.map(issue => `
<div class="other-issue-item ${issue.severity}">
<span class="issue-type">${issue.validation_type || 'Issue'}</span>
<p class="issue-message">${issue.message || 'No description available'}</p>
</div>
`).join('')}
</div>
</div>
`;
}
function extractRatio(message) {
const ratioMatch = message.match(/(\d+\.?\d*):1/);
return ratioMatch ? `${ratioMatch[1]}:1` : 'N/A';
}
function extractValues(message) {
const requestMatch = message.match(/Request: ([\d\.]+[mMi]?)/);
const limitMatch = message.match(/Limit: ([\d\.]+[mMi]?)/);
const request = requestMatch ? requestMatch[1] : 'N/A';
const limit = limitMatch ? limitMatch[1] : 'N/A';
return `${request}${limit}`;
}
function generateSpecificRecommendation(issue, resourceType) {
const message = issue.message || '';
const ratioMatch = message.match(/(\d+\.?\d*):1/);
const requestMatch = message.match(/Request: ([\d\.]+[mMi]?)/);
if (!ratioMatch || !requestMatch) {
return `Set ${resourceType} limit to 3x the request value`;
}
const currentRatio = parseFloat(ratioMatch[1]);
const requestValue = parseFloat(requestMatch[1]);
const unit = requestMatch[1].replace(/[\d\.]/g, '');
if (currentRatio > 3.0) {
const recommendedLimit = Math.round(requestValue * 3.0);
const recommendedLimitStr = recommendedLimit + unit;
return `Set ${resourceType} limit to ${recommendedLimitStr} (3:1 ratio)`;
} else {
return `${resourceType} ratio is acceptable (${ratioMatch[1]}:1)`;
}
}
// Action button functions
function applyResourceFix(podName) {
console.log(`Applying resource fix for pod: ${podName}`);
// TODO: Implement actual fix application
alert(`Resource fix for ${podName} would be applied here.\n\nThis feature will:\n- Generate YAML patch\n- Apply to cluster\n- Verify changes\n\nImplementation coming in next phase.`);
}
function viewYamlPatch(podName) {
console.log(`Viewing YAML patch for pod: ${podName}`);
// TODO: Implement YAML patch generation
alert(`YAML patch for ${podName} would be shown here.\n\nThis feature will:\n- Generate strategic merge patch\n- Show before/after comparison\n- Allow copy to clipboard\n\nImplementation coming in next phase.`);
}
// Dashboard Charts Functions
async function loadDashboardCharts() {
try {
// Load all charts in parallel with individual error handling
const chartPromises = [
loadResourceUtilizationTrend().catch(err => {
console.warn('Resource utilization trend failed:', err);
return null;
}),
loadNamespaceDistribution().catch(err => {
console.warn('Namespace distribution failed:', err);
return null;
}),
loadIssuesTimeline().catch(err => {
console.warn('Issues timeline failed:', err);
return null;
}),
loadTopWorkloads().catch(err => {
console.warn('Top workloads failed:', err);
return null;
}),
loadOvercommitByNamespace().catch(err => {
console.warn('Overcommit by namespace failed:', err);
return null;
})
];
// Wait for all charts to complete (or fail gracefully)
await Promise.allSettled(chartPromises);
// Update progress for each successful chart
let successCount = 0;
chartPromises.forEach((promise, index) => {
promise.then(result => {
if (result !== null) {
successCount++;
updateSmartProgress(2, `Loaded ${successCount}/5 charts successfully`);
}
});
});
console.log('Dashboard charts loading completed');
} catch (error) {
console.error('Error loading dashboard charts:', error);
}
}
// 1. Resource Utilization Trend (24h)
async function loadResourceUtilizationTrend() {
try {
// Update progress
updateSmartProgress(2, 'Loading resource utilization trend...');
// Use real Prometheus data from historical analysis
const response = await fetch('/api/v1/optimized/historical/summary');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
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);
// Update loading state
loadingState.charts.resourceUtilization = true;
} 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: 16, angle: -45, textAnchor: 'end' }
},
tickFormat: (t) => `${t}%`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
scale: 'time',
tickCount: 8,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 16, angle: -45, textAnchor: 'end' }
},
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 {
// Update progress
updateSmartProgress(2, 'Loading namespace distribution...');
const response = await fetch('/api/v1/namespace-distribution');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Pass data directly to chart function (no mapping needed)
createNamespaceDistributionChart(data.distribution, data);
// Update loading state
loadingState.charts.namespaceDistribution = true;
} 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'];
// Calculate total CPU for percentages
const totalCpu = data.reduce((sum, item) => sum + item.cpu_requests, 0);
// Sort by CPU usage and take top 10
const sortedData = data
.sort((a, b) => b.cpu_requests - a.cpu_requests)
.slice(0, 10);
// Create horizontal bar chart for better readability
let chartHtml = '<div style="padding: 20px; height: 100%; overflow-y: auto;">';
sortedData.forEach((item, index) => {
const percentage = data.reduce((sum, d) => sum + d.cpu_requests, 0) > 0 ?
(item.cpu_requests / data.reduce((sum, d) => sum + d.cpu_requests, 0)) * 100 : 0;
chartHtml += `
<div style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span style="color: var(--pf-global--Color--100); font-size: 14px; font-weight: 500;">
${item.namespace}
</span>
<span style="color: var(--pf-global--Color--200); font-size: 12px;">
${percentage.toFixed(1)}%
</span>
</div>
<div style="
width: 100%;
height: 8px;
background: var(--pf-global--BackgroundColor--200);
border-radius: 4px;
overflow: hidden;
">
<div style="
width: ${percentage}%;
height: 100%;
background: linear-gradient(90deg, #0066CC, #4ECDC4);
border-radius: 4px;
transition: width 0.3s ease;
"></div>
</div>
</div>
`;
});
chartHtml += '</div>';
// Clear container and render chart
container.innerHTML = chartHtml;
}
// 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);
// Update loading state
loadingState.charts.issuesTimeline = true;
} 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: 16, angle: -45, textAnchor: 'end' }
}
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
scale: 'time',
tickCount: 7,
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 16, angle: -45, textAnchor: 'end' }
},
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);
// Update loading state
loadingState.charts.topWorkloads = true;
} 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: 16, angle: -45, textAnchor: 'end' }
},
tickFormat: (t) => `${t}m`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 16, angle: -45, textAnchor: 'end' }
}
}),
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 {
const response = await fetch('/api/v1/overcommit-by-namespace');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Transform data for chart
const overcommitData = data.overcommit.map(item => ({
namespace: item.namespace,
cpu: item.cpu_overcommit,
memory: item.memory_overcommit
}));
createOvercommitNamespaceChart(overcommitData);
// Update loading state
loadingState.charts.overcommitByNamespace = true;
} 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: 16, angle: -45, textAnchor: 'end' }
},
tickFormat: (t) => `${t}%`
}),
React.createElement(Victory.VictoryAxis, {
key: 'x-axis',
style: {
axis: { stroke: '#666' },
tickLabels: { fill: '#ccc', fontSize: 16, angle: -45, textAnchor: 'end' }
}
}),
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
// Show error message
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) {
console.log('updateMetricsCards called with data:', data);
try {
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;
console.log('updateMetricsCards completed successfully');
} catch (error) {
console.error('Error in updateMetricsCards:', error);
}
// 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, critical: 0 }
};
}
namespaceGroups[namespace].validations.push(validation);
namespaceGroups[namespace].pods.add(validation.pod_name);
// Count severity - CORRIGIDO para incluir critical
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++;
} else if (validation.severity === 'critical') {
namespaceGroups[namespace].severity_breakdown.critical++;
}
});
}
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
<div class="severity-breakdown">
${getSeverityBreakdown(namespace)}
</div>
</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.critical > 0) return 'danger';
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.critical > 0) return 'Critical';
if (breakdown.error > 0) return 'Error';
if (breakdown.warning > 0) return 'Warning';
if (breakdown.info > 0) return 'Info';
return 'Healthy';
}
function getSeverityBreakdown(namespace) {
const breakdown = namespace.severity_breakdown || {};
const parts = [];
if (breakdown.critical > 0) parts.push(`<span class="critical-count">${breakdown.critical} critical</span>`);
if (breakdown.error > 0) parts.push(`<span class="error-count">${breakdown.error} errors</span>`);
if (breakdown.warning > 0) parts.push(`<span class="warning-count">${breakdown.warning} warnings</span>`);
if (breakdown.info > 0) parts.push(`<span class="info-count">${breakdown.info} info</span>`);
return parts.join(' • ');
}
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);
// Skip metrics-grid completely - don't show any error for it
if (containerId === 'metrics-grid') {
console.error('Error for metrics-grid (ignored):', message);
return;
}
// For other containers, replace content as before
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>