3415 lines
141 KiB
HTML
3415 lines
141 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ORU Analyzer - OpenShift Resource Usage Analyzer</title>
|
||
|
||
<!-- Red Hat Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@300;400;500;600;700&family=Red+Hat+Text:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
|
||
<!-- PatternFly 6.3.1 CSS -->
|
||
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@6.3.1/patternfly.css">
|
||
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@6.3.1/patternfly-addons.css">
|
||
|
||
<!-- Font Awesome for icons -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
|
||
|
||
<!-- Custom OpenShift-like styles -->
|
||
<style>
|
||
:root {
|
||
/* OpenShift Color Palette */
|
||
--pf-global--primary-color--100: #0066CC;
|
||
--pf-global--primary-color--200: #004080;
|
||
--pf-global--success-color--100: #3E8635;
|
||
--pf-global--warning-color--100: #F0AB00;
|
||
--pf-global--danger-color--100: #C9190B;
|
||
--pf-global--info-color--100: #009596;
|
||
|
||
/* Dark Theme Colors */
|
||
--pf-global--BackgroundColor--100: #151515;
|
||
--pf-global--BackgroundColor--200: #1E1E1E;
|
||
--pf-global--BackgroundColor--300: #2B2B2B;
|
||
--pf-global--Color--100: #FFFFFF;
|
||
--pf-global--Color--200: #F0F0F0;
|
||
--pf-global--Color--300: #D2D2D2;
|
||
--pf-global--Color--400: #8A8A8A;
|
||
|
||
/* Typography */
|
||
--pf-global--FontFamily--sans-serif: 'Red Hat Text', 'Helvetica Neue', Arial, sans-serif;
|
||
--pf-global--FontFamily--heading: 'Red Hat Display', 'Helvetica Neue', Arial, sans-serif;
|
||
}
|
||
|
||
/* Dark Mode Base */
|
||
body {
|
||
background-color: var(--pf-global--BackgroundColor--100);
|
||
color: var(--pf-global--Color--100);
|
||
font-family: var(--pf-global--FontFamily--sans-serif);
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
/* OpenShift-like Header */
|
||
.openshift-header {
|
||
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
|
||
border-bottom: 1px solid #404040;
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 24px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.openshift-header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.openshift-header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.hamburger-menu {
|
||
color: var(--pf-global--Color--100);
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.hamburger-menu:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.openshift-logo {
|
||
color: var(--pf-global--Color--100);
|
||
font-family: var(--pf-global--FontFamily--heading);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.header-icon {
|
||
color: var(--pf-global--Color--100);
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
position: relative;
|
||
}
|
||
|
||
.header-icon:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.notification-badge {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 4px;
|
||
background-color: var(--pf-global--danger-color--100);
|
||
color: white;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
min-width: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.user-dropdown {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--pf-global--Color--100);
|
||
cursor: pointer;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.user-dropdown:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
/* OpenShift-like Sidebar */
|
||
.openshift-sidebar {
|
||
background-color: #000000;
|
||
width: 280px;
|
||
height: calc(100vh - 60px);
|
||
position: fixed;
|
||
left: 0;
|
||
top: 60px;
|
||
overflow-y: auto;
|
||
border-right: 1px solid #404040;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.sidebar-section {
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #404040;
|
||
}
|
||
|
||
.sidebar-section-title {
|
||
color: var(--pf-global--Color--400);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
padding: 0 24px 8px;
|
||
margin: 0;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.sidebar-nav-item {
|
||
margin: 0;
|
||
}
|
||
|
||
.sidebar-nav-link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: var(--pf-global--Color--100);
|
||
text-decoration: none;
|
||
padding: 12px 24px;
|
||
transition: all 0.2s;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
|
||
.sidebar-nav-link:hover {
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.sidebar-nav-link.active {
|
||
background-color: rgba(0, 102, 204, 0.1);
|
||
border-left-color: var(--pf-global--primary-color--100);
|
||
color: var(--pf-global--primary-color--100);
|
||
}
|
||
|
||
.sidebar-nav-icon {
|
||
font-size: 16px;
|
||
width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.sidebar-nav-arrow {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
/* Main Content Area */
|
||
.main-content {
|
||
margin-left: 280px;
|
||
padding: 24px;
|
||
background-color: var(--pf-global--BackgroundColor--100);
|
||
min-height: calc(100vh - 60px);
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.page-title {
|
||
font-family: var(--pf-global--FontFamily--heading);
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: var(--pf-global--Color--100);
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.page-description {
|
||
color: var(--pf-global--Color--300);
|
||
font-size: 16px;
|
||
margin: 0;
|
||
}
|
||
|
||
/* OpenShift-like Cards */
|
||
.dashboard-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.openshift-card {
|
||
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
|
||
border: 1px solid #404040;
|
||
border-radius: 8px;
|
||
padding: 24px;
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.openshift-card:hover {
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.card-title {
|
||
font-family: var(--pf-global--FontFamily--heading);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--pf-global--Color--100);
|
||
margin: 0;
|
||
}
|
||
|
||
.card-action {
|
||
color: var(--pf-global--primary-color--100);
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.card-action:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Metrics Cards */
|
||
.metrics-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.metric-card {
|
||
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
|
||
border: 1px solid #404040;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.metric-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.metric-value {
|
||
font-family: var(--pf-global--FontFamily--heading);
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: var(--pf-global--Color--100);
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.metric-label {
|
||
color: var(--pf-global--Color--300);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
margin: 0;
|
||
}
|
||
|
||
.metric-icon {
|
||
font-size: 24px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.metric-icon.success {
|
||
color: var(--pf-global--success-color--100);
|
||
}
|
||
|
||
.metric-icon.warning {
|
||
color: var(--pf-global--warning-color--100);
|
||
}
|
||
|
||
.metric-icon.danger {
|
||
color: var(--pf-global--danger-color--100);
|
||
}
|
||
|
||
.metric-icon.info {
|
||
color: var(--pf-global--info-color--100);
|
||
}
|
||
|
||
/* Status Indicators */
|
||
.status-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px 12px;
|
||
border-radius: 16px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.status-indicator.success {
|
||
background-color: rgba(62, 134, 53, 0.2);
|
||
color: var(--pf-global--success-color--100);
|
||
border: 1px solid rgba(62, 134, 53, 0.3);
|
||
}
|
||
|
||
.status-indicator.warning {
|
||
background-color: rgba(240, 171, 0, 0.2);
|
||
color: var(--pf-global--warning-color--100);
|
||
border: 1px solid rgba(240, 171, 0, 0.3);
|
||
}
|
||
|
||
.status-indicator.danger {
|
||
background-color: rgba(201, 25, 11, 0.2);
|
||
color: var(--pf-global--danger-color--100);
|
||
border: 1px solid rgba(201, 25, 11, 0.3);
|
||
}
|
||
|
||
.status-indicator.info {
|
||
background-color: rgba(0, 149, 150, 0.2);
|
||
color: var(--pf-global--info-color--100);
|
||
border: 1px solid rgba(0, 149, 150, 0.3);
|
||
}
|
||
|
||
/* Tables */
|
||
.openshift-table {
|
||
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
|
||
border: 1px solid #404040;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.table-header {
|
||
background: linear-gradient(135deg, #404040 0%, #2B2B2B 100%);
|
||
padding: 16px 24px;
|
||
border-bottom: 1px solid #404040;
|
||
}
|
||
|
||
.table-title {
|
||
font-family: var(--pf-global--FontFamily--heading);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--pf-global--Color--100);
|
||
margin: 0;
|
||
}
|
||
|
||
.table-content {
|
||
padding: 0;
|
||
}
|
||
|
||
.table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.table th {
|
||
background-color: #404040;
|
||
color: var(--pf-global--Color--100);
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
padding: 12px 24px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #404040;
|
||
}
|
||
|
||
.table td {
|
||
color: var(--pf-global--Color--200);
|
||
font-size: 14px;
|
||
padding: 12px 24px;
|
||
border-bottom: 1px solid #404040;
|
||
}
|
||
|
||
.table tr:hover {
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
/* Buttons */
|
||
.priority-critical {
|
||
background-color: var(--pf-global--danger-color--100);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.priority-high {
|
||
background-color: var(--pf-global--warning-color--100);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.priority-medium {
|
||
background-color: var(--pf-global--info-color--100);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.priority-low {
|
||
background-color: var(--pf-global--Color--400);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
/* PatternFly Code Editor Styles */
|
||
.pf-v6-c-code-editor {
|
||
background-color: var(--pf-global--BackgroundColor--100);
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
border-radius: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__header {
|
||
background-color: var(--pf-global--BackgroundColor--200);
|
||
border-bottom: 1px solid var(--pf-global--BorderColor--200);
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__header-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__controls {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__controls-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__header-main {
|
||
color: var(--pf-global--Color--100);
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
background-color: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--pf-global--Color--100);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
background-color: var(--pf-global--BackgroundColor--300);
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
}
|
||
|
||
.pf-v6-c-code-editor__tab-icon {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__code {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pf-v6-c-code-editor__code-pre {
|
||
flex: 1;
|
||
background-color: #1e1e1e;
|
||
color: #d4d4d4;
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
margin: 0;
|
||
padding: 16px;
|
||
overflow: auto;
|
||
white-space: pre;
|
||
word-wrap: normal;
|
||
}
|
||
|
||
.pf-v6-c-button.pf-m-plain {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--pf-global--Color--200);
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.pf-v6-c-button.pf-m-plain:hover {
|
||
background-color: var(--pf-global--BackgroundColor--300);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.openshift-button {
|
||
background: linear-gradient(135deg, var(--pf-global--primary-color--100) 0%, var(--pf-global--primary-color--200) 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.openshift-button:hover {
|
||
background: linear-gradient(135deg, var(--pf-global--primary-color--200) 0%, #003366 100%);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 8px rgba(0, 102, 204, 0.3);
|
||
}
|
||
|
||
.openshift-button.secondary {
|
||
background: linear-gradient(135deg, #404040 0%, #2B2B2B 100%);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.openshift-select {
|
||
background-color: var(--pf-global--Color--200);
|
||
color: var(--pf-global--Color--100);
|
||
border: 1px solid var(--pf-global--Color--300);
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
font-size: 14px;
|
||
min-width: 150px;
|
||
}
|
||
|
||
.openshift-select:focus {
|
||
outline: none;
|
||
border-color: var(--pf-global--primary-color--100);
|
||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
|
||
}
|
||
|
||
.info-icon {
|
||
cursor: pointer;
|
||
margin-left: 8px;
|
||
color: var(--pf-global--info-color--100);
|
||
font-size: 14px;
|
||
opacity: 0.7;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
.info-icon:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.overcommit-details {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.overcommit-details h3 {
|
||
color: var(--pf-global--Color--100);
|
||
margin-bottom: 16px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.metric-detail {
|
||
margin-bottom: 12px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--pf-global--BorderColor--200);
|
||
}
|
||
|
||
.metric-detail:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.metric-detail strong {
|
||
color: var(--pf-global--Color--100);
|
||
display: inline-block;
|
||
min-width: 140px;
|
||
}
|
||
|
||
.metric-detail ul {
|
||
margin: 8px 0 0 20px;
|
||
color: var(--pf-global--Color--200);
|
||
}
|
||
|
||
.metric-detail li {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.openshift-button.secondary:hover {
|
||
background: linear-gradient(135deg, #505050 0%, #404040 100%);
|
||
}
|
||
|
||
/* Loading States */
|
||
.loading-spinner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px;
|
||
color: var(--pf-global--Color--300);
|
||
}
|
||
|
||
.spinner {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid #404040;
|
||
border-top: 2px solid var(--pf-global--primary-color--100);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.openshift-sidebar {
|
||
transform: translateX(-100%);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.openshift-sidebar.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.dashboard-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.metrics-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
/* Hidden sections */
|
||
.section-hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
/* Accordion Styles */
|
||
.expand-button {
|
||
background: none;
|
||
border: none;
|
||
color: var(--pf-global--Color--100);
|
||
cursor: pointer;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.expand-button:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.workload-details-row {
|
||
background-color: #1A1A1A;
|
||
}
|
||
|
||
.workload-details-container {
|
||
padding: 0;
|
||
}
|
||
|
||
.workload-row:hover {
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
/* Workload Accordion Styles */
|
||
.workloads-accordion {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.workload-accordion-item {
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
border-radius: 8px;
|
||
margin-bottom: 12px;
|
||
background-color: var(--pf-global--BackgroundColor--100);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.workload-accordion-header {
|
||
padding: 16px 20px;
|
||
background-color: var(--pf-global--BackgroundColor--200);
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.workload-accordion-header:hover {
|
||
background-color: var(--pf-global--BackgroundColor--300);
|
||
}
|
||
|
||
.workload-accordion-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.workload-accordion-icon {
|
||
transition: transform 0.2s ease;
|
||
color: var(--pf-global--Color--400);
|
||
}
|
||
|
||
.workload-accordion-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.workload-stat {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: var(--pf-global--Color--300);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.workload-stat i {
|
||
color: var(--pf-global--Color--400);
|
||
}
|
||
|
||
.workload-accordion-content {
|
||
background-color: var(--pf-global--BackgroundColor--100);
|
||
border-top: 1px solid var(--pf-global--BorderColor--200);
|
||
}
|
||
|
||
.workload-issues-container {
|
||
padding: 20px;
|
||
}
|
||
|
||
.workload-issues-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.workload-issue-item {
|
||
background-color: var(--pf-global--BackgroundColor--200);
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.workload-issue-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.workload-issue-title {
|
||
font-weight: 600;
|
||
color: var(--pf-global--Color--100);
|
||
flex: 1;
|
||
}
|
||
|
||
.workload-issue-pod {
|
||
color: var(--pf-global--Color--300);
|
||
font-size: 14px;
|
||
background-color: var(--pf-global--BackgroundColor--300);
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.workload-issue-content {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.workload-issue-message {
|
||
color: var(--pf-global--Color--200);
|
||
margin: 0 0 8px 0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.workload-issue-recommendation {
|
||
background-color: var(--pf-global--BackgroundColor--300);
|
||
border-left: 3px solid var(--pf-global--info-color--100);
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.workload-issue-recommendation strong {
|
||
color: var(--pf-global--info-color--100);
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.modal {
|
||
display: block;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: var(--pf-global--BackgroundColor--100);
|
||
margin: 5% auto;
|
||
padding: 0;
|
||
border-radius: 8px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||
animation: modalSlideIn 0.3s ease-out;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
min-width: 400px;
|
||
}
|
||
|
||
@keyframes modalSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-50px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.modal-header {
|
||
background: linear-gradient(135deg, #2B2B2B 0%, #1E1E1E 100%);
|
||
color: var(--pf-global--Color--100);
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #404040;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.close {
|
||
color: var(--pf-global--Color--300);
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.close:hover {
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 24px;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.status-indicator {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-indicator.danger {
|
||
background-color: var(--pf-global--danger-color--100);
|
||
color: white;
|
||
}
|
||
|
||
.status-indicator.warning {
|
||
background-color: var(--pf-global--warning-color--100);
|
||
color: black;
|
||
}
|
||
|
||
.status-indicator.info {
|
||
background-color: var(--pf-global--info-color--100);
|
||
color: white;
|
||
}
|
||
|
||
.status-indicator.success {
|
||
background-color: var(--pf-global--success-color--100);
|
||
color: white;
|
||
}
|
||
|
||
/* Smart Recommendations Gallery Styles */
|
||
.gallery-container {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||
gap: 20px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.service-card {
|
||
background: var(--pf-global--BackgroundColor--100);
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
transition: all 0.2s ease;
|
||
position: relative;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.service-card:hover {
|
||
border-color: var(--pf-global--primary-color--100);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.service-card.selected {
|
||
border-color: var(--pf-global--primary-color--100);
|
||
background: var(--pf-global--primary-color--50);
|
||
}
|
||
|
||
.service-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.service-card-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
background: var(--pf-global--primary-color--100);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 16px;
|
||
color: white;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.service-card-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--pf-global--Color--100);
|
||
margin: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.service-card-checkbox {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.service-card-body {
|
||
flex: 1;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.service-card-description {
|
||
color: var(--pf-global--Color--200);
|
||
margin-bottom: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.service-card-meta {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.service-card-meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
color: var(--pf-global--Color--300);
|
||
}
|
||
|
||
.service-card-priority {
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.priority-high {
|
||
background: var(--pf-global--danger-color--100);
|
||
color: white;
|
||
}
|
||
|
||
.priority-medium {
|
||
background: var(--pf-global--warning-color--100);
|
||
color: var(--pf-global--Color--100);
|
||
}
|
||
|
||
.priority-low {
|
||
background: var(--pf-global--success-color--100);
|
||
color: white;
|
||
}
|
||
|
||
.service-card-footer {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.service-card-footer .openshift-button {
|
||
font-size: 12px;
|
||
padding: 8px 16px;
|
||
flex: 1;
|
||
min-width: 120px;
|
||
}
|
||
|
||
/* Bulk Select Styles */
|
||
.bulk-select-toolbar {
|
||
background: var(--pf-global--BackgroundColor--200);
|
||
border-bottom: 1px solid var(--pf-global--BorderColor--200);
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu {
|
||
background: var(--pf-global--BackgroundColor--100);
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
border-radius: 4px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
z-index: 1000;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu-item {
|
||
padding: 8px 16px;
|
||
color: var(--pf-global--Color--100);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu-item:hover {
|
||
background: var(--pf-global--BackgroundColor--200);
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle {
|
||
background: var(--pf-global--BackgroundColor--100);
|
||
border: 1px solid var(--pf-global--BorderColor--200);
|
||
color: var(--pf-global--Color--100);
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle:hover {
|
||
border-color: var(--pf-global--primary-color--100);
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle-icon {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle[aria-expanded="true"] .pf-v6-c-dropdown__toggle-icon {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
/* PatternFly Dropdown Styles */
|
||
.pf-v6-c-dropdown {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle {
|
||
background: var(--pf-global--BackgroundColor--100);
|
||
border: 1px solid var(--pf-global--BorderColor--300);
|
||
color: var(--pf-global--Color--100);
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-width: 150px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle:hover {
|
||
background: var(--pf-global--BackgroundColor--200);
|
||
border-color: var(--pf-global--BorderColor--400);
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle:focus {
|
||
outline: 2px solid var(--pf-global--active-color--100);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle-text {
|
||
flex: 1;
|
||
text-align: left;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__toggle-icon {
|
||
margin-left: 8px;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
background: var(--pf-global--BackgroundColor--100);
|
||
border: 1px solid var(--pf-global--BorderColor--300);
|
||
border-radius: 4px;
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||
z-index: 1000;
|
||
margin-top: 4px;
|
||
padding: 4px 0;
|
||
list-style: none;
|
||
margin: 0;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu-item {
|
||
background: none;
|
||
border: none;
|
||
color: var(--pf-global--Color--100);
|
||
padding: 8px 12px;
|
||
width: 100%;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
display: block;
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu-item:hover {
|
||
background: var(--pf-global--BackgroundColor--200);
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu-item.pf-m-selected {
|
||
background: var(--pf-global--active-color--100);
|
||
color: var(--pf-global--Color--light-100);
|
||
}
|
||
|
||
.pf-v6-c-dropdown__menu-item:focus {
|
||
outline: 2px solid var(--pf-global--active-color--100);
|
||
outline-offset: -2px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- OpenShift-like Header -->
|
||
<header class="openshift-header">
|
||
<div class="openshift-header-left">
|
||
<a href="#" class="openshift-logo">ORU Analyzer</a>
|
||
</div>
|
||
<div class="openshift-header-right">
|
||
<div class="header-icon">
|
||
<i class="fas fa-th"></i>
|
||
</div>
|
||
<div class="header-icon" style="position: relative;">
|
||
<i class="fas fa-bell"></i>
|
||
<span class="notification-badge">46</span>
|
||
</div>
|
||
<div class="header-icon">
|
||
<i class="fas fa-plus"></i>
|
||
</div>
|
||
<div class="header-icon">
|
||
<i class="fas fa-question-circle"></i>
|
||
</div>
|
||
<div class="user-dropdown">
|
||
<i class="fas fa-user-circle"></i>
|
||
<span>anobre</span>
|
||
<i class="fas fa-chevron-down"></i>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- OpenShift-like Sidebar -->
|
||
<nav class="openshift-sidebar" id="sidebar">
|
||
<div class="sidebar-section">
|
||
<div class="sidebar-nav">
|
||
<li class="sidebar-nav-item">
|
||
<a href="#" class="sidebar-nav-link active" data-section="workload-scanner">
|
||
<i class="fas fa-home sidebar-nav-icon"></i>
|
||
<span>Home</span>
|
||
</a>
|
||
</li>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section">
|
||
<h3 class="sidebar-section-title">Analysis</h3>
|
||
<ul class="sidebar-nav">
|
||
<li class="sidebar-nav-item">
|
||
<a href="#" class="sidebar-nav-link" data-section="requests-limits">
|
||
<i class="fas fa-exclamation-triangle sidebar-nav-icon"></i>
|
||
<span>Requests & Limits</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="sidebar-section">
|
||
<h3 class="sidebar-section-title">Recommendations</h3>
|
||
<ul class="sidebar-nav">
|
||
<li class="sidebar-nav-item">
|
||
<a href="#" class="sidebar-nav-link" data-section="historical-analysis">
|
||
<i class="fas fa-chart-line sidebar-nav-icon"></i>
|
||
<span>Historical Based</span>
|
||
</a>
|
||
</li>
|
||
<li class="sidebar-nav-item">
|
||
<a href="#" class="sidebar-nav-link" data-section="vpa-management">
|
||
<i class="fas fa-cogs sidebar-nav-icon"></i>
|
||
<span>VPA Management</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="sidebar-section">
|
||
<h3 class="sidebar-section-title">Settings</h3>
|
||
<ul class="sidebar-nav">
|
||
<li class="sidebar-nav-item">
|
||
<a href="#" class="sidebar-nav-link" data-section="settings">
|
||
<i class="fas fa-cog sidebar-nav-icon"></i>
|
||
<span>Configuration</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Main Content -->
|
||
<main class="main-content">
|
||
<!-- Workload Scanner Section -->
|
||
<section id="workload-scanner-section">
|
||
<div class="page-header">
|
||
<h1 class="page-title">OpenShift Resource Usage Analyzer - Dashboard</h1>
|
||
<p class="page-description">Identify and analyze workloads with resource configuration issues</p>
|
||
</div>
|
||
|
||
<!-- Metrics Cards -->
|
||
<div class="metrics-grid" id="metrics-grid">
|
||
<div class="metric-card">
|
||
<div class="metric-icon success">
|
||
<i class="fas fa-cube"></i>
|
||
</div>
|
||
<div class="metric-value" id="total-workloads">-</div>
|
||
<div class="metric-label">Total Workloads</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-icon info">
|
||
<i class="fas fa-layer-group"></i>
|
||
</div>
|
||
<div class="metric-value" id="total-namespaces">-</div>
|
||
<div class="metric-label">Namespaces</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-icon danger">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
</div>
|
||
<div class="metric-value" id="critical-issues">-</div>
|
||
<div class="metric-label">Critical Issues</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-icon warning">
|
||
<i class="fas fa-exclamation-circle"></i>
|
||
</div>
|
||
<div class="metric-value" id="total-warnings">-</div>
|
||
<div class="metric-label">Warnings</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cluster Overcommit Summary -->
|
||
<div class="metrics-grid" style="margin-top: 24px;">
|
||
<div class="metric-card">
|
||
<div class="metric-icon info">
|
||
<i class="fas fa-microchip"></i>
|
||
</div>
|
||
<div class="metric-value" id="cpu-overcommit">-</div>
|
||
<div class="metric-label">
|
||
CPU Overcommit
|
||
<span class="info-icon" onclick="showOvercommitDetails('cpu')" title="Click for details">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-icon warning">
|
||
<i class="fas fa-memory"></i>
|
||
</div>
|
||
<div class="metric-value" id="memory-overcommit">-</div>
|
||
<div class="metric-label">
|
||
Memory Overcommit
|
||
<span class="info-icon" onclick="showOvercommitDetails('memory')" title="Click for details">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-icon danger">
|
||
<i class="fas fa-folder-open"></i>
|
||
</div>
|
||
<div class="metric-value" id="namespaces-in-overcommit">-</div>
|
||
<div class="metric-label">Namespaces in Overcommit</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-icon success">
|
||
<i class="fas fa-chart-pie"></i>
|
||
</div>
|
||
<div class="metric-value" id="resource-utilization">-</div>
|
||
<div class="metric-label">
|
||
Resource Utilization
|
||
<span class="info-icon" onclick="showResourceUtilizationDetails()" title="Click for details">ℹ️</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- Requests & Limits Section -->
|
||
<section id="requests-limits-section" class="section-hidden">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Requests & Limits Analysis</h1>
|
||
<p class="page-description">Analyze workloads with resource configuration issues and missing requests/limits</p>
|
||
</div>
|
||
|
||
<!-- Workloads Table Card -->
|
||
<div class="openshift-card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Workloads with Issues</h2>
|
||
<button class="openshift-button" id="refresh-workloads">
|
||
<i class="fas fa-sync-alt"></i>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
<div class="table-content" id="workloads-table-container">
|
||
<div class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
Loading workloads...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<!-- VPA Management Section -->
|
||
<section id="vpa-management-section" class="section-hidden">
|
||
<div class="page-header">
|
||
<h1 class="page-title">VPA Management & Recommendations</h1>
|
||
<p class="page-description">Manage Vertical Pod Autoscaler configurations, monitor VPA status, and apply smart recommendations</p>
|
||
</div>
|
||
|
||
<!-- VPA Management Content -->
|
||
<div class="openshift-card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">VPA Status</h2>
|
||
<button class="openshift-button" onclick="loadVPAManagement()">
|
||
<i class="fas fa-sync-alt"></i>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
<div class="table-content" id="vpa-management-container">
|
||
<div class="loading-spinner">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
Loading VPA data...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Smart Recommendations Content -->
|
||
<div class="openshift-card" style="margin-top: 24px;">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Smart Recommendations</h2>
|
||
<div class="card-actions">
|
||
<button class="openshift-button" onclick="loadSmartRecommendations()">
|
||
<i class="fas fa-sync-alt"></i>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bulk Select Toolbar -->
|
||
<div class="bulk-select-toolbar" style="padding: 16px; border-bottom: 1px solid var(--pf-global--BorderColor--200);">
|
||
<div class="pf-v6-c-toolbar">
|
||
<div class="pf-v6-c-toolbar__content">
|
||
<div class="pf-v6-c-toolbar__content-section">
|
||
<div class="pf-v6-c-toolbar__group pf-m-toggle-group">
|
||
<div class="pf-v6-c-toolbar__item">
|
||
<div class="pf-v6-c-dropdown">
|
||
<button class="pf-v6-c-dropdown__toggle" type="button" id="bulk-select-toggle" aria-expanded="false" onclick="toggleBulkSelect()">
|
||
<span class="pf-v6-c-dropdown__toggle-text">
|
||
<span id="bulk-select-text">0 selected</span>
|
||
</span>
|
||
<span class="pf-v6-c-dropdown__toggle-icon">
|
||
<i class="fas fa-chevron-down"></i>
|
||
</span>
|
||
</button>
|
||
<ul class="pf-v6-c-dropdown__menu" id="bulk-select-menu" style="display: none;">
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="selectAllRecommendations()">
|
||
Select all (<span id="total-recommendations">0</span>)
|
||
</button>
|
||
</li>
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="selectPageRecommendations()">
|
||
Select page (<span id="page-recommendations">0</span>)
|
||
</button>
|
||
</li>
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="deselectAllRecommendations()">
|
||
Select none (0)
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="pf-v6-c-toolbar__item" id="bulk-actions" style="display: none;">
|
||
<button class="openshift-button openshift-button-primary" onclick="applySelectedRecommendations()">
|
||
<i class="fas fa-check"></i>
|
||
Apply Selected (<span id="selected-count">0</span>)
|
||
</button>
|
||
<button class="openshift-button" onclick="previewSelectedRecommendations()">
|
||
<i class="fas fa-eye"></i>
|
||
Preview Selected
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gallery of Service Cards -->
|
||
<div class="gallery-container" id="smart-recommendations-container" style="padding: 20px;">
|
||
<div class="loading-spinner">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
Loading recommendations...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Historical Analysis Section -->
|
||
<section id="historical-analysis-section" class="section-hidden">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Historical Analysis</h1>
|
||
<p class="page-description">Resource consumption analysis and historical data</p>
|
||
</div>
|
||
|
||
<!-- Workloads List Card -->
|
||
<div class="openshift-card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Available Workloads</h2>
|
||
<div style="display: flex; gap: 12px; align-items: center;">
|
||
<div class="pf-v6-c-dropdown" id="timeRangeDropdown">
|
||
<button class="pf-v6-c-dropdown__toggle" type="button" id="timeRangeToggle" aria-expanded="false" onclick="toggleTimeRangeDropdown()">
|
||
<span class="pf-v6-c-dropdown__toggle-text" id="timeRangeText">Last 24 Hours</span>
|
||
<span class="pf-v6-c-dropdown__toggle-icon">
|
||
<i class="fas fa-chevron-down"></i>
|
||
</span>
|
||
</button>
|
||
<ul class="pf-v6-c-dropdown__menu" id="timeRangeMenu" style="display: none;">
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('1h', 'Last 1 Hour')">
|
||
Last 1 Hour
|
||
</button>
|
||
</li>
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('6h', 'Last 6 Hours')">
|
||
Last 6 Hours
|
||
</button>
|
||
</li>
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item pf-m-selected" onclick="selectTimeRange('24h', 'Last 24 Hours')">
|
||
Last 24 Hours
|
||
</button>
|
||
</li>
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('7d', 'Last 7 Days')">
|
||
Last 7 Days
|
||
</button>
|
||
</li>
|
||
<li>
|
||
<button class="pf-v6-c-dropdown__menu-item" onclick="selectTimeRange('30d', 'Last 30 Days')">
|
||
Last 30 Days
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<button class="openshift-button" id="refresh-historical">
|
||
<i class="fas fa-sync-alt"></i>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="table-content" id="historical-workloads-container">
|
||
<div class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
Loading historical data...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Workload Details Card (hidden initially) -->
|
||
<div class="openshift-card section-hidden" id="workload-details-container">
|
||
<div class="card-header">
|
||
<h2 class="card-title" id="workload-details-title">Workload Details</h2>
|
||
<button class="openshift-button secondary" id="close-workload-details">
|
||
<i class="fas fa-times"></i>
|
||
Close
|
||
</button>
|
||
</div>
|
||
<div class="table-content" id="workload-details-content">
|
||
<!-- Workload details will be populated here -->
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Settings Section -->
|
||
<section id="settings-section" class="section-hidden">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Configuration Settings</h1>
|
||
<p class="page-description">Configure analysis thresholds, ratios, and parameters for resource optimization</p>
|
||
</div>
|
||
|
||
<!-- Settings Content -->
|
||
<div class="openshift-card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Analysis Parameters</h2>
|
||
<button class="openshift-button" onclick="loadSettings()">
|
||
<i class="fas fa-sync-alt"></i>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
<div class="table-content" id="settings-container">
|
||
<div class="loading-spinner">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
Loading settings...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<!-- JavaScript -->
|
||
<script>
|
||
// Global variables
|
||
let currentData = null;
|
||
let currentSection = 'workload-scanner';
|
||
|
||
// Initialize the application
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializeApp();
|
||
});
|
||
|
||
function initializeApp() {
|
||
// Setup navigation
|
||
setupNavigation();
|
||
|
||
// Load initial data
|
||
loadWorkloadScanner();
|
||
}
|
||
|
||
function setupNavigation() {
|
||
// Sidebar navigation
|
||
const navLinks = document.querySelectorAll('.sidebar-nav-link[data-section]');
|
||
navLinks.forEach(link => {
|
||
link.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
const section = this.getAttribute('data-section');
|
||
showSection(section);
|
||
});
|
||
});
|
||
|
||
|
||
// Close workload details
|
||
document.getElementById('close-workload-details').addEventListener('click', function() {
|
||
document.getElementById('workload-details-container').classList.add('section-hidden');
|
||
});
|
||
|
||
// Refresh buttons
|
||
document.getElementById('refresh-workloads').addEventListener('click', loadRequestsLimits);
|
||
document.getElementById('refresh-historical').addEventListener('click', loadHistoricalAnalysis);
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', function(event) {
|
||
const dropdown = document.getElementById('timeRangeDropdown');
|
||
if (dropdown && !dropdown.contains(event.target)) {
|
||
const menu = document.getElementById('timeRangeMenu');
|
||
const toggle = document.getElementById('timeRangeToggle');
|
||
if (menu && toggle) {
|
||
menu.style.display = 'none';
|
||
toggle.setAttribute('aria-expanded', 'false');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function showSection(section) {
|
||
// Hide all sections
|
||
document.querySelectorAll('main section').forEach(sec => {
|
||
sec.classList.add('section-hidden');
|
||
});
|
||
|
||
// Show selected section
|
||
document.getElementById(section + '-section').classList.remove('section-hidden');
|
||
|
||
// Update active nav item
|
||
document.querySelectorAll('.sidebar-nav-link').forEach(link => {
|
||
link.classList.remove('active');
|
||
});
|
||
document.querySelector(`.sidebar-nav-link[data-section="${section}"]`).classList.add('active');
|
||
|
||
currentSection = section;
|
||
|
||
// Load section data
|
||
if (section === 'workload-scanner') {
|
||
loadWorkloadScanner();
|
||
} else if (section === 'requests-limits') {
|
||
loadRequestsLimits();
|
||
} else if (section === 'vpa-management') {
|
||
loadVPAManagement();
|
||
} else if (section === 'historical-analysis') {
|
||
loadHistoricalAnalysis();
|
||
} else if (section === 'settings') {
|
||
loadSettings();
|
||
}
|
||
}
|
||
|
||
async function loadWorkloadScanner() {
|
||
try {
|
||
// Load cluster status
|
||
const clusterResponse = await fetch('/api/v1/cluster/status');
|
||
const clusterData = await clusterResponse.json();
|
||
|
||
currentData = { cluster: clusterData };
|
||
|
||
// Update metrics cards
|
||
updateMetricsCards(clusterData);
|
||
|
||
} catch (error) {
|
||
console.error('Error loading workload scanner data:', error);
|
||
showError('metrics-grid', 'Failed to load cluster data');
|
||
}
|
||
}
|
||
|
||
async function loadRequestsLimits() {
|
||
try {
|
||
showLoading('workloads-table-container');
|
||
|
||
// Load validations
|
||
const validationsResponse = await fetch('/api/v1/validations');
|
||
const validationsData = await validationsResponse.json();
|
||
|
||
currentData = { validations: validationsData };
|
||
|
||
// Update workloads accordion
|
||
updateWorkloadsTable(validationsData);
|
||
|
||
// Pre-load all workload details
|
||
await preloadAllWorkloadDetails();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading requests & limits data:', error);
|
||
showError('workloads-table-container', 'Failed to load workload data');
|
||
}
|
||
}
|
||
|
||
async function preloadAllWorkloadDetails() {
|
||
if (!window.workloadsData) return;
|
||
|
||
try {
|
||
// Load all validations by namespace
|
||
const response = await fetch('/api/v1/validations/by-namespace');
|
||
const data = await response.json();
|
||
|
||
// Store the data for each namespace
|
||
window.workloadDetails = {};
|
||
if (data.namespaces) {
|
||
data.namespaces.forEach(namespace => {
|
||
window.workloadDetails[namespace.namespace] = {
|
||
validations: namespace.pods ? Object.values(namespace.pods).flatMap(pod => pod.validations) : []
|
||
};
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading namespace details:', error);
|
||
window.workloadDetails = {};
|
||
}
|
||
}
|
||
|
||
function toggleWorkloadIssues(index) {
|
||
const content = document.getElementById(`workload-content-${index}`);
|
||
const icon = document.getElementById(`workload-icon-${index}`);
|
||
|
||
if (content.style.display === 'none') {
|
||
// Expand accordion
|
||
content.style.display = 'block';
|
||
icon.classList.remove('fa-chevron-right');
|
||
icon.classList.add('fa-chevron-down');
|
||
|
||
// Load issues if not already loaded
|
||
loadWorkloadIssues(index);
|
||
} else {
|
||
// Collapse accordion
|
||
content.style.display = 'none';
|
||
icon.classList.remove('fa-chevron-down');
|
||
icon.classList.add('fa-chevron-right');
|
||
}
|
||
}
|
||
|
||
function loadWorkloadIssues(index) {
|
||
const namespace = window.workloadsData[index];
|
||
const container = document.getElementById(`workload-issues-${index}`);
|
||
|
||
if (!namespace) return;
|
||
|
||
// Check if we already have the data
|
||
if (window.workloadDetails && window.workloadDetails[namespace.namespace]) {
|
||
const data = window.workloadDetails[namespace.namespace];
|
||
|
||
if (data.error) {
|
||
container.innerHTML = `
|
||
<div class="error-message">
|
||
<i class="fas fa-exclamation-circle"></i>
|
||
${data.error}
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Display the issues
|
||
displayWorkloadIssues(container, data, namespace);
|
||
return;
|
||
}
|
||
|
||
// If data not available, show loading
|
||
container.innerHTML = `
|
||
<div class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
Loading issues...
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function displayWorkloadIssues(container, data, namespace) {
|
||
if (!data.validations || data.validations.length === 0) {
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 20px; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-check-circle" style="font-size: 24px; margin-bottom: 8px; color: var(--pf-global--success-color--100);"></i>
|
||
<p style="margin: 0;">No issues found for this namespace</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
const issuesHTML = `
|
||
<div class="workload-issues-list">
|
||
${data.validations.map(validation => `
|
||
<div class="workload-issue-item">
|
||
<div class="workload-issue-header">
|
||
<span class="severity-badge ${validation.severity}">
|
||
${validation.severity.toUpperCase()}
|
||
</span>
|
||
<span class="workload-issue-title">${validation.validation_type || validation.title || 'Resource Issue'}</span>
|
||
<span class="workload-issue-pod">${validation.pod_name}</span>
|
||
</div>
|
||
<div class="workload-issue-content">
|
||
<p class="workload-issue-message">${validation.message || validation.description || 'No description available'}</p>
|
||
${validation.recommendation || validation.action_required ? `
|
||
<div class="workload-issue-recommendation">
|
||
<strong>Recommendation:</strong> ${validation.recommendation || validation.action_required}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = issuesHTML;
|
||
}
|
||
|
||
// Load settings
|
||
async function loadSettings() {
|
||
try {
|
||
showLoading('settings-container');
|
||
|
||
// TODO: Implement settings loading
|
||
// For now, show placeholder
|
||
document.getElementById('settings-container').innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-cog" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
|
||
<h3>Settings Configuration</h3>
|
||
<p>Configure analysis thresholds, ratios, and parameters for resource optimization.</p>
|
||
<p style="margin-top: 16px; font-style: italic;">Coming soon...</p>
|
||
</div>
|
||
`;
|
||
|
||
} catch (error) {
|
||
console.error('Error loading settings:', error);
|
||
showError('settings-container', 'Failed to load settings');
|
||
}
|
||
}
|
||
|
||
// Load smart recommendations
|
||
async function loadSmartRecommendations() {
|
||
try {
|
||
showLoading('smart-recommendations-container');
|
||
|
||
const response = await fetch('/api/v1/smart-recommendations');
|
||
const data = await response.json();
|
||
updateSmartRecommendations(data);
|
||
} catch (error) {
|
||
console.error('Error loading smart recommendations:', error);
|
||
document.getElementById('smart-recommendations-container').innerHTML =
|
||
'<div class="error-message">Failed to load recommendations</div>';
|
||
}
|
||
}
|
||
|
||
// Update smart recommendations
|
||
function updateSmartRecommendations(data) {
|
||
const container = document.getElementById('smart-recommendations-container');
|
||
|
||
// Store recommendations globally for button functions
|
||
window.currentRecommendations = data.categories || [];
|
||
window.selectedRecommendations = new Set();
|
||
|
||
if (!data || !data.categories || data.categories.length === 0) {
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-lightbulb" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
|
||
<h3>No Recommendations Available</h3>
|
||
<p>No smart recommendations found for the current cluster state.</p>
|
||
</div>
|
||
`;
|
||
updateBulkSelectUI();
|
||
return;
|
||
}
|
||
|
||
// Update bulk select counters
|
||
document.getElementById('total-recommendations').textContent = data.categories.length;
|
||
document.getElementById('page-recommendations').textContent = data.categories.length;
|
||
|
||
const recommendationsHtml = data.categories.map((workload, index) => {
|
||
// Determine priority based on priority_score
|
||
let priority = 'low';
|
||
if (workload.priority_score >= 6) priority = 'high';
|
||
else if (workload.priority_score >= 4) priority = 'medium';
|
||
|
||
// Determine recommendation type based on resource config status
|
||
let recommendationType = 'vpa_activation';
|
||
let title = `Activate VPA for ${workload.workload_name}`;
|
||
let description = `Enable VPA for ${workload.workload_name} to get automatic resource recommendations based on usage patterns.`;
|
||
|
||
if (workload.resource_config_status === 'missing_requests') {
|
||
recommendationType = 'resource_config';
|
||
title = `Configure Resources for ${workload.workload_name}`;
|
||
description = `Add missing resource requests and limits for ${workload.workload_name} to improve resource management.`;
|
||
} else if (workload.resource_config_status === 'suboptimal_ratio') {
|
||
recommendationType = 'ratio_adjustment';
|
||
title = `Optimize Resource Ratios for ${workload.workload_name}`;
|
||
description = `Adjust CPU to memory ratio for ${workload.workload_name} to optimize resource allocation.`;
|
||
}
|
||
|
||
return `
|
||
<div class="service-card" id="recommendation-${index}" data-recommendation-id="${index}">
|
||
<div class="service-card-header">
|
||
<div class="service-card-icon">
|
||
<i class="fas fa-${getRecommendationIcon(recommendationType)}"></i>
|
||
</div>
|
||
<h3 class="service-card-title">${title}</h3>
|
||
<div class="service-card-checkbox">
|
||
<input type="checkbox"
|
||
id="checkbox-${index}"
|
||
class="recommendation-checkbox"
|
||
onchange="toggleRecommendationSelection(${index})"
|
||
style="transform: scale(1.2);">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-card-body">
|
||
<p class="service-card-description">${description}</p>
|
||
|
||
<div class="service-card-meta">
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-cube"></i>
|
||
<span>${workload.workload_name}</span>
|
||
</div>
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-layer-group"></i>
|
||
<span>${workload.namespace}</span>
|
||
</div>
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-tag"></i>
|
||
<span>${recommendationType}</span>
|
||
</div>
|
||
<div class="service-card-priority priority-${priority}">
|
||
${priority.toUpperCase()}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-card-meta">
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-chart-line"></i>
|
||
<span>Score: ${workload.priority_score}/10</span>
|
||
</div>
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-bolt"></i>
|
||
<span>Impact: ${workload.estimated_impact}</span>
|
||
</div>
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-history"></i>
|
||
<span>Age: ${workload.age_days} days</span>
|
||
</div>
|
||
<div class="service-card-meta-item">
|
||
<i class="fas fa-check-circle" style="color: ${workload.vpa_candidate ? '#28a745' : '#dc3545'};"></i>
|
||
<span>VPA Ready</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-card-footer">
|
||
<button class="openshift-button openshift-button-primary" onclick="downloadVPAYAML('${workload.workload_name}', '${workload.namespace}')">
|
||
<i class="fas fa-download"></i>
|
||
VPA YAML
|
||
</button>
|
||
|
||
<button class="openshift-button openshift-button-success" onclick="applySmartRecommendation('${workload.workload_name}', '${workload.namespace}', '${recommendationType}', '${priority}')">
|
||
<i class="fas fa-check"></i>
|
||
Apply
|
||
</button>
|
||
|
||
<button class="openshift-button" onclick="previewSmartRecommendation('${workload.workload_name}', '${workload.namespace}', '${recommendationType}', '${priority}')">
|
||
<i class="fas fa-eye"></i>
|
||
Preview
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = recommendationsHtml;
|
||
updateBulkSelectUI();
|
||
}
|
||
|
||
// Bulk Select Functions
|
||
function toggleBulkSelect() {
|
||
const menu = document.getElementById('bulk-select-menu');
|
||
const toggle = document.getElementById('bulk-select-toggle');
|
||
const isOpen = menu.style.display !== 'none';
|
||
|
||
menu.style.display = isOpen ? 'none' : 'block';
|
||
toggle.setAttribute('aria-expanded', !isOpen);
|
||
}
|
||
|
||
function toggleRecommendationSelection(index) {
|
||
const checkbox = document.getElementById(`checkbox-${index}`);
|
||
const card = document.getElementById(`recommendation-${index}`);
|
||
|
||
if (checkbox.checked) {
|
||
window.selectedRecommendations.add(index);
|
||
card.classList.add('selected');
|
||
} else {
|
||
window.selectedRecommendations.delete(index);
|
||
card.classList.remove('selected');
|
||
}
|
||
|
||
updateBulkSelectUI();
|
||
}
|
||
|
||
function selectAllRecommendations() {
|
||
const checkboxes = document.querySelectorAll('.recommendation-checkbox');
|
||
checkboxes.forEach((checkbox, index) => {
|
||
checkbox.checked = true;
|
||
window.selectedRecommendations.add(index);
|
||
document.getElementById(`recommendation-${index}`).classList.add('selected');
|
||
});
|
||
updateBulkSelectUI();
|
||
toggleBulkSelect();
|
||
}
|
||
|
||
function selectPageRecommendations() {
|
||
const checkboxes = document.querySelectorAll('.recommendation-checkbox');
|
||
checkboxes.forEach((checkbox, index) => {
|
||
checkbox.checked = true;
|
||
window.selectedRecommendations.add(index);
|
||
document.getElementById(`recommendation-${index}`).classList.add('selected');
|
||
});
|
||
updateBulkSelectUI();
|
||
toggleBulkSelect();
|
||
}
|
||
|
||
function deselectAllRecommendations() {
|
||
const checkboxes = document.querySelectorAll('.recommendation-checkbox');
|
||
checkboxes.forEach((checkbox, index) => {
|
||
checkbox.checked = false;
|
||
window.selectedRecommendations.delete(index);
|
||
document.getElementById(`recommendation-${index}`).classList.remove('selected');
|
||
});
|
||
updateBulkSelectUI();
|
||
toggleBulkSelect();
|
||
}
|
||
|
||
function updateBulkSelectUI() {
|
||
const selectedCount = window.selectedRecommendations ? window.selectedRecommendations.size : 0;
|
||
const totalCount = window.currentRecommendations ? window.currentRecommendations.length : 0;
|
||
|
||
document.getElementById('bulk-select-text').textContent = `${selectedCount} selected`;
|
||
document.getElementById('selected-count').textContent = selectedCount;
|
||
|
||
const bulkActions = document.getElementById('bulk-actions');
|
||
if (selectedCount > 0) {
|
||
bulkActions.style.display = 'flex';
|
||
} else {
|
||
bulkActions.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function applySelectedRecommendations() {
|
||
if (!window.selectedRecommendations || window.selectedRecommendations.size === 0) {
|
||
alert('No recommendations selected');
|
||
return;
|
||
}
|
||
|
||
const selectedIndices = Array.from(window.selectedRecommendations);
|
||
const selectedRecommendations = selectedIndices.map(index => window.currentRecommendations[index]);
|
||
|
||
try {
|
||
showLoading('smart-recommendations-container');
|
||
|
||
const results = [];
|
||
for (const rec of selectedRecommendations) {
|
||
try {
|
||
const response = await fetch('/api/v1/recommendations/apply', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
...rec,
|
||
dry_run: false
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
results.push({ success: true, recommendation: rec.title, result });
|
||
} else {
|
||
results.push({ success: false, recommendation: rec.title, error: 'Failed to apply' });
|
||
}
|
||
} catch (error) {
|
||
results.push({ success: false, recommendation: rec.title, error: error.message });
|
||
}
|
||
}
|
||
|
||
// Show results
|
||
const successCount = results.filter(r => r.success).length;
|
||
const failCount = results.filter(r => !r.success).length;
|
||
|
||
alert(`Applied ${successCount} recommendations successfully. ${failCount} failed.`);
|
||
|
||
// Refresh recommendations
|
||
loadSmartRecommendations();
|
||
|
||
} catch (error) {
|
||
console.error('Error applying selected recommendations:', error);
|
||
alert('Error applying recommendations: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function previewSelectedRecommendations() {
|
||
if (!window.selectedRecommendations || window.selectedRecommendations.size === 0) {
|
||
alert('No recommendations selected');
|
||
return;
|
||
}
|
||
|
||
const selectedIndices = Array.from(window.selectedRecommendations);
|
||
const selectedRecommendations = selectedIndices.map(index => window.currentRecommendations[index]);
|
||
|
||
try {
|
||
const results = [];
|
||
for (const rec of selectedRecommendations) {
|
||
try {
|
||
const response = await fetch('/api/v1/recommendations/apply', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
...rec,
|
||
dry_run: true
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
results.push({ success: true, recommendation: rec, result });
|
||
} else {
|
||
results.push({ success: false, recommendation: rec, error: 'Failed to preview' });
|
||
}
|
||
} catch (error) {
|
||
results.push({ success: false, recommendation: rec, error: error.message });
|
||
}
|
||
}
|
||
|
||
showRecommendationPreview(results);
|
||
|
||
} catch (error) {
|
||
console.error('Error previewing selected recommendations:', error);
|
||
alert('Error previewing recommendations: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Load VPA management
|
||
async function loadVPAManagement() {
|
||
try {
|
||
showLoading('vpa-management-container');
|
||
|
||
const response = await fetch('/api/v1/vpa-status');
|
||
const data = await response.json();
|
||
updateVPAManagement(data);
|
||
} catch (error) {
|
||
console.error('Error loading VPA management:', error);
|
||
document.getElementById('vpa-management-container').innerHTML =
|
||
'<div class="error-message">Failed to load VPA data</div>';
|
||
}
|
||
}
|
||
|
||
// Update VPA management
|
||
function updateVPAManagement(data) {
|
||
const container = document.getElementById('vpa-management-container');
|
||
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-cogs" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--Color--400);"></i>
|
||
<h3>VPA Management</h3>
|
||
<p>VPA management features coming soon...</p>
|
||
<p style="font-size: 14px; color: var(--pf-global--Color--400);">
|
||
This section will show VPA status, configurations, and management options.
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Helper functions for recommendations
|
||
function getRecommendationIcon(type) {
|
||
switch(type) {
|
||
case 'vpa_activation': return 'fa-rocket';
|
||
case 'resource_config': return 'fa-cog';
|
||
case 'ratio_adjustment': return 'fa-balance-scale';
|
||
default: return 'fa-lightbulb';
|
||
}
|
||
}
|
||
|
||
function getPriorityColor(priority) {
|
||
switch(priority) {
|
||
case 'critical': return 'var(--pf-global--danger-color--100)';
|
||
case 'high': return 'var(--pf-global--warning-color--100)';
|
||
case 'medium': return 'var(--pf-global--info-color--100)';
|
||
case 'low': return 'var(--pf-global--Color--300)';
|
||
default: return 'var(--pf-global--Color--300)';
|
||
}
|
||
}
|
||
|
||
// Show VPA YAML in Code Editor Modal
|
||
function downloadVPAYAML(workloadName, namespace) {
|
||
// Find the recommendation data
|
||
const recommendations = window.currentRecommendations || [];
|
||
const recommendation = recommendations.find(rec =>
|
||
rec.workload_name === workloadName && rec.namespace === namespace
|
||
);
|
||
|
||
if (!recommendation || !recommendation.vpa_yaml) {
|
||
alert('VPA YAML not available for this recommendation');
|
||
return;
|
||
}
|
||
|
||
// Create modal with PatternFly Code Editor
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.style.display = 'block';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 90%; max-width: 1200px; min-width: 800px; height: 80vh;">
|
||
<div class="modal-header">
|
||
<h3>VPA YAML - ${workloadName}</h3>
|
||
<div style="display: flex; gap: 12px; align-items: center;">
|
||
<button class="openshift-button" onclick="downloadVPAYAMLFile('${workloadName}', '${namespace}')">
|
||
<i class="fas fa-download"></i>
|
||
Download
|
||
</button>
|
||
<button class="openshift-button" onclick="copyVPAYAMLToClipboard()">
|
||
<i class="fas fa-copy"></i>
|
||
Copy
|
||
</button>
|
||
<span class="close" onclick="this.closest('.modal').remove()">×</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()">×</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()">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p><strong>Namespace:</strong> ${namespace}</p>
|
||
<p><strong>Commands:</strong></p>
|
||
<pre style="background-color: #1e1e1e; padding: 16px; border-radius: 4px; overflow-x: auto; color: var(--pf-global--Color--100);">${recommendation.kubectl_commands.join('\n')}</pre>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// Close modal when clicking outside
|
||
modal.addEventListener('click', function(e) {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
async function loadHistoricalAnalysis() {
|
||
try {
|
||
showLoading('historical-workloads-container');
|
||
|
||
// Get selected time range from PatternFly dropdown
|
||
const timeRangeText = document.getElementById('timeRangeText')?.textContent || 'Last 24 Hours';
|
||
let timeRange = '24h'; // default
|
||
|
||
switch(timeRangeText) {
|
||
case 'Last 1 Hour': timeRange = '1h'; break;
|
||
case 'Last 6 Hours': timeRange = '6h'; break;
|
||
case 'Last 24 Hours': timeRange = '24h'; break;
|
||
case 'Last 7 Days': timeRange = '7d'; break;
|
||
case 'Last 30 Days': timeRange = '30d'; break;
|
||
}
|
||
|
||
// Load historical data
|
||
const response = await fetch(`/api/v1/historical-analysis?time_range=${timeRange}`);
|
||
const data = await response.json();
|
||
|
||
updateHistoricalWorkloads(data);
|
||
|
||
} catch (error) {
|
||
console.error('Error loading historical analysis data:', error);
|
||
showError('historical-workloads-container', 'Failed to load historical data');
|
||
}
|
||
}
|
||
|
||
// Alias for the select onchange
|
||
function loadHistoricalWorkloads() {
|
||
loadHistoricalAnalysis();
|
||
}
|
||
|
||
// Time range dropdown functions
|
||
function toggleTimeRangeDropdown() {
|
||
const menu = document.getElementById('timeRangeMenu');
|
||
const toggle = document.getElementById('timeRangeToggle');
|
||
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
|
||
|
||
menu.style.display = isExpanded ? 'none' : 'block';
|
||
toggle.setAttribute('aria-expanded', !isExpanded);
|
||
}
|
||
|
||
function selectTimeRange(value, text) {
|
||
// Update selected item
|
||
document.querySelectorAll('#timeRangeMenu .pf-m-selected').forEach(item => {
|
||
item.classList.remove('pf-m-selected');
|
||
});
|
||
event.target.classList.add('pf-m-selected');
|
||
|
||
// Update display text
|
||
document.getElementById('timeRangeText').textContent = text;
|
||
|
||
// Close dropdown
|
||
document.getElementById('timeRangeMenu').style.display = 'none';
|
||
document.getElementById('timeRangeToggle').setAttribute('aria-expanded', 'false');
|
||
|
||
// Load data with new time range
|
||
loadHistoricalAnalysisWithTimeRange(value);
|
||
}
|
||
|
||
async function loadHistoricalAnalysisWithTimeRange(timeRange) {
|
||
try {
|
||
showLoading('historical-workloads-container');
|
||
|
||
// Load historical data
|
||
const response = await fetch(`/api/v1/historical-analysis?time_range=${timeRange}`);
|
||
const data = await response.json();
|
||
|
||
updateHistoricalWorkloads(data);
|
||
|
||
} catch (error) {
|
||
console.error('Error loading historical analysis data:', error);
|
||
showError('historical-workloads-container', 'Failed to load historical data');
|
||
}
|
||
}
|
||
|
||
function updateMetricsCards(data) {
|
||
document.getElementById('total-workloads').textContent = data.total_pods || 0;
|
||
document.getElementById('total-namespaces').textContent = data.total_namespaces || 0;
|
||
document.getElementById('critical-issues').textContent = data.total_errors || 0;
|
||
document.getElementById('total-warnings').textContent = data.total_warnings || 0;
|
||
|
||
// Update overcommit metrics
|
||
if (data.overcommit) {
|
||
document.getElementById('cpu-overcommit').textContent = `${data.overcommit.cpu_overcommit_percent || 0}%`;
|
||
document.getElementById('memory-overcommit').textContent = `${data.overcommit.memory_overcommit_percent || 0}%`;
|
||
document.getElementById('namespaces-in-overcommit').textContent = data.overcommit.namespaces_in_overcommit || 0;
|
||
document.getElementById('resource-utilization').textContent = `${(data.overcommit.resource_utilization || 0).toFixed(1)}%`;
|
||
|
||
// Store overcommit data for modal display
|
||
window.overcommitData = data.overcommit;
|
||
} else {
|
||
document.getElementById('cpu-overcommit').textContent = '0%';
|
||
document.getElementById('memory-overcommit').textContent = '0%';
|
||
document.getElementById('namespaces-in-overcommit').textContent = '0';
|
||
document.getElementById('resource-utilization').textContent = '0%';
|
||
}
|
||
}
|
||
|
||
function updateWorkloadsTable(data) {
|
||
const container = document.getElementById('workloads-table-container');
|
||
|
||
// Group validations by namespace
|
||
const namespaceGroups = {};
|
||
if (data.validations && data.validations.length > 0) {
|
||
data.validations.forEach(validation => {
|
||
const namespace = validation.namespace;
|
||
if (!namespaceGroups[namespace]) {
|
||
namespaceGroups[namespace] = {
|
||
namespace: namespace,
|
||
validations: [],
|
||
pods: new Set(),
|
||
severity_breakdown: { error: 0, warning: 0, info: 0 }
|
||
};
|
||
}
|
||
namespaceGroups[namespace].validations.push(validation);
|
||
namespaceGroups[namespace].pods.add(validation.pod_name);
|
||
|
||
// Count severity
|
||
if (validation.severity === 'error') {
|
||
namespaceGroups[namespace].severity_breakdown.error++;
|
||
} else if (validation.severity === 'warning') {
|
||
namespaceGroups[namespace].severity_breakdown.warning++;
|
||
} else if (validation.severity === 'info') {
|
||
namespaceGroups[namespace].severity_breakdown.info++;
|
||
}
|
||
});
|
||
}
|
||
|
||
const namespaces = Object.values(namespaceGroups);
|
||
|
||
if (namespaces.length === 0) {
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--success-color--100);"></i>
|
||
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Issues Found</h3>
|
||
<p style="margin: 0;">All workloads are properly configured</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Create accordion HTML
|
||
const accordionHTML = `
|
||
<div class="workloads-accordion">
|
||
${namespaces.map((namespace, index) => `
|
||
<div class="workload-accordion-item">
|
||
<div class="workload-accordion-header" onclick="toggleWorkloadIssues(${index})" id="workload-header-${index}">
|
||
<div class="workload-accordion-title">
|
||
<i class="fas fa-chevron-right workload-accordion-icon" id="workload-icon-${index}"></i>
|
||
<strong style="color: var(--pf-global--Color--100);">${namespace.namespace}</strong>
|
||
</div>
|
||
<div class="workload-accordion-stats">
|
||
<span class="workload-stat">
|
||
<i class="fas fa-cube"></i>
|
||
${namespace.pods.size} pods
|
||
</span>
|
||
<span class="workload-stat">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
${namespace.validations.length} issues
|
||
</span>
|
||
<span class="status-indicator ${getSeverityClass(namespace)}">
|
||
${getSeverityText(namespace)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="workload-accordion-content" id="workload-content-${index}" style="display: none;">
|
||
<div class="workload-issues-container" id="workload-issues-${index}">
|
||
<div class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
Loading issues...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = accordionHTML;
|
||
|
||
// Store namespace data globally for accordion functions
|
||
window.workloadsData = namespaces;
|
||
}
|
||
|
||
function updateHistoricalWorkloads(data) {
|
||
const container = document.getElementById('historical-workloads-container');
|
||
|
||
if (!data.workloads || data.workloads.length === 0) {
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-chart-line" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--info-color--100);"></i>
|
||
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">No Historical Data</h3>
|
||
<p style="margin: 0;">No workloads available for analysis</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
const tableHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 30px;"></th>
|
||
<th>Workload</th>
|
||
<th>Namespace</th>
|
||
<th>Pods</th>
|
||
<th>CPU Usage</th>
|
||
<th>Memory Usage</th>
|
||
<th>Last Updated</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${data.workloads.map((workload, index) => `
|
||
<tr class="workload-row" data-workload="${workload.name}" data-namespace="${workload.namespace}">
|
||
<td>
|
||
<button class="expand-button" onclick="toggleWorkloadDetails(${index})" id="expand-btn-${index}">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
</td>
|
||
<td>
|
||
<strong style="color: var(--pf-global--Color--100);">${workload.name}</strong>
|
||
</td>
|
||
<td>${workload.namespace}</td>
|
||
<td>${workload.pod_count || 0}</td>
|
||
<td>${workload.cpu_usage || 'N/A'}</td>
|
||
<td>${workload.memory_usage || 'N/A'}</td>
|
||
<td>${workload.last_updated ? new Date(workload.last_updated).toLocaleString() : 'N/A'}</td>
|
||
<td>
|
||
<span style="color: var(--pf-global--Color--300); font-size: 12px;">
|
||
Click to expand
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
<tr class="workload-details-row" id="details-${index}" style="display: none;">
|
||
<td colspan="8">
|
||
<div class="workload-details-container" id="details-content-${index}">
|
||
<div class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
Loading workload details...
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
container.innerHTML = tableHTML;
|
||
}
|
||
|
||
function showWorkloadDetails(workloadName, namespace) {
|
||
// Update title
|
||
document.getElementById('workload-details-title').textContent = `${workloadName} - ${namespace}`;
|
||
|
||
// Load workload details
|
||
loadWorkloadDetails(workloadName, namespace);
|
||
|
||
// Show details container
|
||
document.getElementById('workload-details-container').classList.remove('section-hidden');
|
||
}
|
||
|
||
|
||
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">×</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">×</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: 50, left: 60, right: 20 },
|
||
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: 50, left: 60, right: 20 },
|
||
domainPadding: { x: 0, y: 0 },
|
||
style: {
|
||
parent: {
|
||
background: '#1E1E1E',
|
||
width: '100%',
|
||
height: '100%'
|
||
}
|
||
}
|
||
}, [
|
||
React.createElement(Victory.VictoryAxis, {
|
||
key: 'y-axis',
|
||
dependentAxis: true,
|
||
style: {
|
||
axis: { stroke: '#404040' },
|
||
tickLabels: { fill: '#FFFFFF', fontSize: 12 },
|
||
grid: { stroke: '#404040' }
|
||
},
|
||
tickFormat: (t) => `${t.toFixed(1)} MB`
|
||
}),
|
||
React.createElement(Victory.VictoryAxis, {
|
||
key: 'x-axis',
|
||
scale: 'time',
|
||
tickCount: 12,
|
||
style: {
|
||
axis: { stroke: '#404040' },
|
||
tickLabels: { fill: '#FFFFFF', fontSize: 12 },
|
||
grid: { stroke: '#404040' }
|
||
},
|
||
tickFormat: (t) => {
|
||
const date = new Date(t);
|
||
// Check if we're dealing with days (7d or 30d)
|
||
if (timeRange === '7d' || timeRange === '30d') {
|
||
return date.toLocaleDateString('pt-BR', {
|
||
day: '2-digit',
|
||
month: '2-digit'
|
||
});
|
||
} else {
|
||
return date.toLocaleTimeString('pt-BR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
timeZone: 'UTC'
|
||
});
|
||
}
|
||
}
|
||
}),
|
||
React.createElement(Victory.VictoryLine, {
|
||
key: 'memory-line',
|
||
data: chartData,
|
||
style: {
|
||
data: {
|
||
stroke: '#CC0000',
|
||
strokeWidth: 2
|
||
}
|
||
},
|
||
animate: {
|
||
duration: 2000,
|
||
onLoad: { duration: 1000 }
|
||
}
|
||
}),
|
||
React.createElement(Victory.VictoryArea, {
|
||
key: 'memory-area',
|
||
data: chartData,
|
||
style: {
|
||
data: {
|
||
fill: 'rgba(204, 0, 0, 0.1)',
|
||
fillOpacity: 0.3
|
||
}
|
||
}
|
||
})
|
||
]);
|
||
|
||
// Render the chart
|
||
ReactDOM.render(chart, container);
|
||
}
|
||
|
||
function updateWorkloadDetails(data) {
|
||
const container = document.getElementById('workload-details-content');
|
||
|
||
container.innerHTML = `
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px;">
|
||
<div class="openshift-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">CPU Usage</h3>
|
||
</div>
|
||
<div style="padding: 20px; text-align: center; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-microchip" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--info-color--100);"></i>
|
||
<p>CPU usage data will be displayed here</p>
|
||
</div>
|
||
</div>
|
||
<div class="openshift-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Memory Usage</h3>
|
||
</div>
|
||
<div style="padding: 20px; text-align: center; color: var(--pf-global--Color--300);">
|
||
<i class="fas fa-memory" style="font-size: 48px; margin-bottom: 16px; color: var(--pf-global--warning-color--100);"></i>
|
||
<p>Memory usage data will be displayed here</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
|
||
function getSeverityClass(namespace) {
|
||
const breakdown = namespace.severity_breakdown || {};
|
||
if (breakdown.error > 0) return 'danger';
|
||
if (breakdown.warning > 0) return 'warning';
|
||
if (breakdown.info > 0) return 'info';
|
||
return 'success';
|
||
}
|
||
|
||
function getSeverityText(namespace) {
|
||
const breakdown = namespace.severity_breakdown || {};
|
||
if (breakdown.error > 0) return 'Error';
|
||
if (breakdown.warning > 0) return 'Warning';
|
||
if (breakdown.info > 0) return 'Info';
|
||
return 'Healthy';
|
||
}
|
||
|
||
function showLoading(containerId) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = `
|
||
<div class="loading-spinner">
|
||
<div class="spinner"></div>
|
||
Loading...
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function showError(containerId, message) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: var(--pf-global--danger-color--100);">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px;"></i>
|
||
<h3 style="margin: 0 0 8px 0; color: var(--pf-global--Color--100);">Error</h3>
|
||
<p style="margin: 0;">${message}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
</script>
|
||
|
||
<!-- React and Victory.js for PatternFly charts -->
|
||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||
<script src="https://unpkg.com/victory@36.6.12/dist/victory.min.js"></script>
|
||
</body>
|
||
</html>
|