From 0a0d3e1f43d69d22076cc6376a1128e01f32250c Mon Sep 17 00:00:00 2001 From: andersonid Date: Thu, 2 Oct 2025 19:16:22 -0300 Subject: [PATCH] feat: implement Smart Recommendations gallery with ServiceCard and BulkSelect - Replace table layout with PatternFly ServiceCard gallery - Add BulkSelect toolbar with select all/page/none options - Implement individual VPA application per workload - Add checkbox selection for each recommendation card - Support bulk apply and preview operations - Improve UX with card-based interface following PatternFly design - Add responsive grid layout for recommendation cards - Implement proper state management for selections --- app/static/index.html | 532 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 464 insertions(+), 68 deletions(-) diff --git a/app/static/index.html b/app/static/index.html index bf61a00..e3224ac 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -830,6 +830,178 @@ 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); + } @@ -1044,24 +1216,79 @@ +
-

Recommendations

-
-
+
+ + +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
- @@ -1254,6 +1481,7 @@ // Store recommendations globally for button functions window.currentRecommendations = data.recommendations || []; + window.selectedRecommendations = new Set(); if (!data || !data.recommendations || data.recommendations.length === 0) { container.innerHTML = ` @@ -1261,77 +1489,245 @@

No Recommendations Available

No smart recommendations found for the current cluster state.

- + `; + updateBulkSelectUI(); return; } - const recommendationsHtml = data.recommendations.map(rec => ` -
-
-

- - ${rec.title} -

- ${rec.priority.toUpperCase()} -
-
-

${rec.description}

- - ${rec.workload_list && rec.workload_list.length > 0 ? ` -
- Affected Workloads: -
    - ${rec.workload_list.map(workload => { - // Extract priority from workload string (e.g., "example (shishika01) - HIGH") - const priorityMatch = workload.match(/\s-\s(\w+)$/); - const priority = priorityMatch ? priorityMatch[1].toLowerCase() : 'low'; - const priorityColor = getPriorityColor(priority); - return `
  • ${workload}
  • `; - }).join('')} -
-
- ` : ''} - - ${rec.implementation_steps && rec.implementation_steps.length > 0 ? ` -
- Implementation Steps: -
    - ${rec.implementation_steps.map(step => `
  1. ${step}
  2. `).join('')} -
-
- ` : ''} - -
- ${rec.vpa_yaml ? ` - - ` : ''} - - ${rec.kubectl_commands && rec.kubectl_commands.length > 0 ? ` - - ` : ''} - - - - + // Update bulk select counters + document.getElementById('total-recommendations').textContent = data.recommendations.length; + document.getElementById('page-recommendations').textContent = data.recommendations.length; + + const recommendationsHtml = data.recommendations.map((rec, index) => ` +
+
+
+
+

${rec.title}

+
+ +
+
+ +
+

${rec.description}

+ +
+
+ + ${rec.workload_name || 'N/A'} +
+
+ + ${rec.namespace || 'N/A'} +
+
+ + ${rec.recommendation_type} +
+
+ ${rec.priority.toUpperCase()} +
+
+
+ +
`).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