diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/index.html b/orchestrator/src/buttercup/orchestrator/ui/static/index.html index ba455589..75f786e0 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/index.html +++ b/orchestrator/src/buttercup/orchestrator/ui/static/index.html @@ -12,6 +12,7 @@

Buttercup CRS Dashboard

@@ -179,6 +180,36 @@ + + +
diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/script.js b/orchestrator/src/buttercup/orchestrator/ui/static/script.js index 49372e6e..5215826e 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/script.js +++ b/orchestrator/src/buttercup/orchestrator/ui/static/script.js @@ -32,6 +32,16 @@ const elements = { loadJsonBtn: document.getElementById('load-json-btn'), loadJsonInput: document.getElementById('load-json-input'), cancelBtn: document.getElementById('cancel-btn'), + submitSarifBtn: document.getElementById('submit-sarif-btn'), + sarifModal: document.getElementById('sarif-modal'), + sarifModalContent: document.querySelector('#sarif-modal .modal-content'), + closeSarifModal: document.getElementById('close-sarif-modal'), + sarifForm: document.getElementById('sarif-form'), + sarifTaskSelect: document.getElementById('sarif-task-select'), + sarifFileInput: document.getElementById('sarif-file-input'), + sarifPreview: document.getElementById('sarif-preview'), + sarifPreviewContent: document.getElementById('sarif-preview-content'), + cancelSarifBtn: document.getElementById('cancel-sarif-btn'), tasksContainer: document.getElementById('tasks-container'), povsContainer: document.getElementById('povs-container'), patchesContainer: document.getElementById('patches-container'), @@ -94,6 +104,21 @@ function setupEventListeners() { }); elements.loadJsonInput.addEventListener('change', handleJsonFileLoad); } + + elements.submitSarifBtn.addEventListener('click', () => { + openSarifModal(); + }); + + elements.closeSarifModal.addEventListener('click', () => { + elements.sarifModal.style.display = 'none'; + }); + + elements.cancelSarifBtn.addEventListener('click', () => { + elements.sarifModal.style.display = 'none'; + }); + + elements.sarifFileInput.addEventListener('change', handleSarifFilePreview); + elements.sarifForm.addEventListener('submit', handleSarifSubmission); elements.refreshBtn.addEventListener('click', loadDashboard); @@ -138,19 +163,24 @@ function setupEventListeners() { let mousePressedOutside = false; window.addEventListener('mousedown', (event) => { - // Check if mouse was pressed outside both modal contents - if (!elements.taskModalContent.contains(event.target) && !elements.detailModalContent.contains(event.target)) { - mousePressedOutside = true; - } else { - mousePressedOutside = false; - } + // Check if mouse was pressed outside all modal contents + const insideAnyModal = + elements.taskModalContent.contains(event.target) || + elements.detailModalContent.contains(event.target) || + elements.sarifModalContent.contains(event.target); + mousePressedOutside = !insideAnyModal; }); window.addEventListener('mouseup', (event) => { // Only close if mouse was pressed outside and released outside - if (mousePressedOutside && !elements.taskModalContent.contains(event.target) && !elements.detailModalContent.contains(event.target)) { + const insideAnyModal = + elements.taskModalContent.contains(event.target) || + elements.detailModalContent.contains(event.target) || + elements.sarifModalContent.contains(event.target); + if (mousePressedOutside && !insideAnyModal) { elements.taskModal.style.display = 'none'; elements.detailModal.style.display = 'none'; + elements.sarifModal.style.display = 'none'; } }); } @@ -800,7 +830,10 @@ function renderTaskDetail(task) { ${task.status === 'active' ? ` -
+
+ @@ -940,6 +973,138 @@ async function cancelTask(taskId) { } } +// Open SARIF modal, optionally pre-selecting a task +function openSarifModal(preselectedTaskId) { + // Populate the task dropdown with active tasks + const activeTaskList = tasks.filter(t => t.status === 'active'); + elements.sarifTaskSelect.innerHTML = + ''; + activeTaskList.forEach(t => { + const label = t.name || t.project_name; + const opt = document.createElement('option'); + opt.value = t.task_id; + opt.textContent = `${label} (${t.task_id.substring(0, 8)}...)`; + elements.sarifTaskSelect.appendChild(opt); + }); + + if (preselectedTaskId) { + elements.sarifTaskSelect.value = preselectedTaskId; + } + + // Reset file input and preview + elements.sarifFileInput.value = ''; + elements.sarifPreview.style.display = 'none'; + elements.sarifPreviewContent.textContent = ''; + + elements.sarifModal.style.display = 'block'; +} + +// Preview SARIF file contents when selected +function handleSarifFilePreview(event) { + const file = event.target.files[0]; + if (!file) { + elements.sarifPreview.style.display = 'none'; + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const parsed = JSON.parse(e.target.result); + const runsCount = (parsed.runs || []).length; + const resultsCount = (parsed.runs || []).reduce( + (sum, run) => sum + (run.results || []).length, + 0, + ); + const version = parsed.version || 'unknown'; + const preview = + `SARIF version: ${version}\n` + + `Runs: ${runsCount}\n` + + `Results: ${resultsCount}\n\n` + + JSON.stringify(parsed, null, 2).substring(0, 500); + elements.sarifPreviewContent.textContent = + preview.length >= 500 ? preview + '\n...' : preview; + elements.sarifPreview.style.display = 'block'; + } catch (err) { + elements.sarifPreviewContent.textContent = + 'Invalid JSON: ' + err.message; + elements.sarifPreview.style.display = 'block'; + } + }; + reader.readAsText(file); +} + +// Handle SARIF form submission +async function handleSarifSubmission(event) { + event.preventDefault(); + + const taskId = elements.sarifTaskSelect.value; + if (!taskId) { + showNotification('Please select a task', 'error'); + return; + } + + const file = elements.sarifFileInput.files[0]; + if (!file) { + showNotification('Please select a SARIF file', 'error'); + return; + } + + const submitBtn = elements.sarifForm.querySelector( + 'button[type="submit"]', + ); + const originalText = submitBtn.textContent; + submitBtn.disabled = true; + submitBtn.innerHTML = ' Submitting...'; + + try { + const text = await file.text(); + const sarif = JSON.parse(text); + + const response = await fetch(`${API_BASE}/webhook/sarif`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ task_id: taskId, sarif: sarif }), + }); + + const result = await response.json(); + + if (response.ok) { + if ( + result.message && + result.message.includes('failed') + ) { + showNotification(result.message, null, result.color); + } else { + showNotification( + result.message || 'SARIF submitted successfully!', + 'success', + ); + } + elements.sarifModal.style.display = 'none'; + } else { + const errorMessage = + result.message || + result.detail || + 'Failed to submit SARIF'; + showNotification(`Error: ${errorMessage}`, 'error'); + } + } catch (error) { + if (error instanceof SyntaxError) { + showNotification( + 'Invalid SARIF file: not valid JSON', + 'error', + ); + } else { + console.error('Error submitting SARIF:', error); + showNotification('Network error occurred', 'error'); + } + } finally { + submitBtn.disabled = false; + submitBtn.textContent = originalText; + } +} + // Approve patch async function approvePatch(taskId, patchId) { try {