feat: implement phase 2 - reorganize accordion layout with pod cards, specific recommendations and action buttons

This commit is contained in:
2025-10-16 14:57:19 -03:00
parent 48f97ed24c
commit 636feb5b2a

View File

@@ -929,6 +929,231 @@
50% { opacity: 0.7; } 50% { opacity: 0.7; }
} }
/* New Pod Card Styles */
.pod-card {
background-color: var(--pf-global--BackgroundColor--200);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 8px;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.pod-card-header {
background: linear-gradient(135deg, var(--pf-global--BackgroundColor--300) 0%, var(--pf-global--BackgroundColor--200) 100%);
padding: 16px 20px;
border-bottom: 1px solid var(--pf-global--BorderColor--200);
display: flex;
align-items: center;
justify-content: space-between;
}
.pod-name-section {
display: flex;
align-items: center;
gap: 12px;
}
.pod-name {
font-size: 18px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.pod-severity-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.pod-severity-badge.warning {
background-color: var(--pf-global--warning-color--100);
color: var(--pf-global--Color--100);
}
.pod-severity-badge.error {
background-color: var(--pf-global--danger-color--100);
color: var(--pf-global--Color--100);
}
.pod-severity-badge.info {
background-color: var(--pf-global--info-color--100);
color: var(--pf-global--Color--100);
}
.pod-stats {
color: var(--pf-global--Color--200);
font-size: 14px;
}
.pod-card-body {
padding: 20px;
}
.resource-section {
margin-bottom: 24px;
}
.resource-section:last-of-type {
margin-bottom: 0;
}
.resource-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--pf-global--BorderColor--300);
}
.resource-type-title {
font-size: 16px;
font-weight: 600;
color: var(--pf-global--Color--100);
margin: 0;
}
.resource-count {
background-color: var(--pf-global--BackgroundColor--300);
color: var(--pf-global--Color--200);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.resource-issues {
display: flex;
flex-direction: column;
gap: 12px;
}
.resource-issue-item {
background-color: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 6px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.issue-details {
display: flex;
align-items: center;
gap: 16px;
}
.issue-ratio {
background-color: var(--pf-global--warning-color--100);
color: var(--pf-global--Color--100);
padding: 4px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
}
.issue-values {
color: var(--pf-global--Color--200);
font-size: 14px;
font-family: 'Courier New', monospace;
}
.issue-recommendation {
background-color: var(--pf-global--success-color--100);
color: var(--pf-global--Color--100);
padding: 12px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.issue-recommendation::before {
content: "💡";
font-size: 16px;
}
.other-issues-section {
margin-bottom: 24px;
}
.other-issues {
display: flex;
flex-direction: column;
gap: 8px;
}
.other-issue-item {
background-color: var(--pf-global--BackgroundColor--100);
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: 6px;
padding: 12px;
}
.issue-type {
font-weight: 600;
color: var(--pf-global--Color--100);
font-size: 14px;
display: block;
margin-bottom: 4px;
}
.issue-message {
color: var(--pf-global--Color--200);
font-size: 14px;
margin: 0;
}
.pod-actions {
display: flex;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--pf-global--BorderColor--200);
}
.action-btn {
padding: 10px 16px;
border-radius: 6px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
text-decoration: none;
}
.apply-fix-btn {
background-color: var(--pf-global--primary-color--100);
color: var(--pf-global--Color--100);
}
.apply-fix-btn:hover {
background-color: var(--pf-global--primary-color--200);
transform: translateY(-1px);
}
.view-yaml-btn {
background-color: transparent;
color: var(--pf-global--Color--200);
border: 1px solid var(--pf-global--BorderColor--200);
}
.view-yaml-btn:hover {
background-color: var(--pf-global--BackgroundColor--300);
color: var(--pf-global--Color--100);
border-color: var(--pf-global--BorderColor--100);
}
.workload-issues-list { .workload-issues-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -2432,33 +2657,182 @@
return; return;
} }
// Group validations by pod for better organization
const podGroups = groupValidationsByPod(data.validations);
const issuesHTML = ` const issuesHTML = `
<div class="workload-issues-list"> <div class="workload-issues-list">
${data.validations.map(validation => ` ${Object.values(podGroups).map(pod => createPodCard(pod)).join('')}
<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> </div>
`; `;
container.innerHTML = issuesHTML; container.innerHTML = issuesHTML;
} }
function groupValidationsByPod(validations) {
const groups = {};
validations.forEach(validation => {
const podName = validation.pod_name;
if (!groups[podName]) {
groups[podName] = {
pod_name: podName,
validations: [],
severity: 'info',
cpuIssues: [],
memoryIssues: [],
otherIssues: []
};
}
groups[podName].validations.push(validation);
// Categorize issues
if (validation.message && validation.message.includes('CPU')) {
groups[podName].cpuIssues.push(validation);
} else if (validation.message && validation.message.includes('Memory')) {
groups[podName].memoryIssues.push(validation);
} else {
groups[podName].otherIssues.push(validation);
}
// Set highest severity
if (validation.severity === 'error' ||
(validation.severity === 'warning' && groups[podName].severity !== 'error')) {
groups[podName].severity = validation.severity;
}
});
return groups;
}
function createPodCard(pod) {
return `
<div class="pod-card">
<div class="pod-card-header">
<div class="pod-name-section">
<h3 class="pod-name">${pod.pod_name}</h3>
<span class="pod-severity-badge ${pod.severity}">
${pod.severity.toUpperCase()}
</span>
</div>
<div class="pod-stats">
<span class="pod-issues-count">${pod.validations.length} issues</span>
</div>
</div>
<div class="pod-card-body">
${createResourceSection('CPU', pod.cpuIssues)}
${createResourceSection('Memory', pod.memoryIssues)}
${createOtherIssuesSection(pod.otherIssues)}
<div class="pod-actions">
<button class="action-btn apply-fix-btn" onclick="applyResourceFix('${pod.pod_name}')">
<i class="fas fa-wrench"></i>
Apply Fix
</button>
<button class="action-btn view-yaml-btn" onclick="viewYamlPatch('${pod.pod_name}')">
<i class="fas fa-code"></i>
View YAML
</button>
</div>
</div>
</div>
`;
}
function createResourceSection(resourceType, issues) {
if (!issues || issues.length === 0) return '';
const recommendations = issues.map(issue => generateSpecificRecommendation(issue, resourceType)).join('');
return `
<div class="resource-section">
<div class="resource-section-header">
<h4 class="resource-type-title">${resourceType} Issues</h4>
<span class="resource-count">${issues.length} issue${issues.length > 1 ? 's' : ''}</span>
</div>
<div class="resource-issues">
${issues.map(issue => `
<div class="resource-issue-item">
<div class="issue-details">
<span class="issue-ratio">${extractRatio(issue.message)}</span>
<span class="issue-values">${extractValues(issue.message)}</span>
</div>
<div class="issue-recommendation">
${generateSpecificRecommendation(issue, resourceType)}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
function createOtherIssuesSection(issues) {
if (!issues || issues.length === 0) return '';
return `
<div class="other-issues-section">
<div class="resource-section-header">
<h4 class="resource-type-title">Other Issues</h4>
<span class="resource-count">${issues.length} issue${issues.length > 1 ? 's' : ''}</span>
</div>
<div class="other-issues">
${issues.map(issue => `
<div class="other-issue-item">
<span class="issue-type">${issue.validation_type || 'Issue'}</span>
<p class="issue-message">${issue.message || 'No description available'}</p>
</div>
`).join('')}
</div>
</div>
`;
}
function extractRatio(message) {
const ratioMatch = message.match(/(\d+\.?\d*):1/);
return ratioMatch ? `${ratioMatch[1]}:1` : 'N/A';
}
function extractValues(message) {
const requestMatch = message.match(/Request: ([\d\.]+[mMi]?)/);
const limitMatch = message.match(/Limit: ([\d\.]+[mMi]?)/);
const request = requestMatch ? requestMatch[1] : 'N/A';
const limit = limitMatch ? limitMatch[1] : 'N/A';
return `${request}${limit}`;
}
function generateSpecificRecommendation(issue, resourceType) {
const message = issue.message || '';
const ratioMatch = message.match(/(\d+\.?\d*):1/);
const requestMatch = message.match(/Request: ([\d\.]+[mMi]?)/);
if (!ratioMatch || !requestMatch) {
return `Set ${resourceType} limit to 3x the request value`;
}
const currentRatio = parseFloat(ratioMatch[1]);
const requestValue = parseFloat(requestMatch[1]);
const unit = requestMatch[1].replace(/[\d\.]/g, '');
if (currentRatio > 3.0) {
const recommendedLimit = Math.round(requestValue * 3.0);
const recommendedLimitStr = recommendedLimit + unit;
return `Set ${resourceType} limit to ${recommendedLimitStr} (3:1 ratio)`;
} else {
return `${resourceType} ratio is acceptable (${ratioMatch[1]}:1)`;
}
}
// Action button functions
function applyResourceFix(podName) {
console.log(`Applying resource fix for pod: ${podName}`);
// TODO: Implement actual fix application
alert(`Resource fix for ${podName} would be applied here.\n\nThis feature will:\n- Generate YAML patch\n- Apply to cluster\n- Verify changes\n\nImplementation coming in next phase.`);
}
function viewYamlPatch(podName) {
console.log(`Viewing YAML patch for pod: ${podName}`);
// TODO: Implement YAML patch generation
alert(`YAML patch for ${podName} would be shown here.\n\nThis feature will:\n- Generate strategic merge patch\n- Show before/after comparison\n- Allow copy to clipboard\n\nImplementation coming in next phase.`);
}
// Dashboard Charts Functions // Dashboard Charts Functions
async function loadDashboardCharts() { async function loadDashboardCharts() {
try { try {