diff --git a/client/src/api/apiService.js b/client/src/api/apiService.js index 1b161e5..e015c01 100644 --- a/client/src/api/apiService.js +++ b/client/src/api/apiService.js @@ -80,6 +80,16 @@ const apiService = { } }, + deleteWorkflowRun: async (id) => { + try { + const response = await api.delete(`${API_URL}/workflow-runs/${id}`); + return response.data.data; + } catch (error) { + console.error(`Error deleting workflow run ${id}:`, error); + throw error; + } + }, + // Sync all workflow runs for a repository syncWorkflowRuns: async (repoName) => { try { diff --git a/client/src/api/socketService.js b/client/src/api/socketService.js index 249fcfc..47ef3ef 100644 --- a/client/src/api/socketService.js +++ b/client/src/api/socketService.js @@ -189,6 +189,13 @@ export const setupSocketListeners = (callbacks) => { debouncedJobsUpdate(data); }); + socket.on('workflowDeleted', (data) => { + console.log('Received workflowDeleted event:', data); + if (callbacks.onWorkflowDeleted) { + callbacks.onWorkflowDeleted(data); + } + }); + // Setup the queue time monitoring const queueMonitorInterval = setInterval(() => checkQueuedWorkflows(alertConfig, callbacks.onLongQueuedWorkflow), 30000); // Check every 30 seconds @@ -200,6 +207,7 @@ export const setupSocketListeners = (callbacks) => { socket.off('workflowUpdate'); socket.off('workflow_update'); socket.off('workflowJobsUpdate'); + socket.off('workflowDeleted'); clearInterval(queueMonitorInterval); lastUpdateTimes.clear(); queuedWorkflows.clear(); diff --git a/client/src/features/workflows/WorkflowHistory.jsx b/client/src/features/workflows/WorkflowHistory.jsx index 04bceaf..e3f9ce3 100644 --- a/client/src/features/workflows/WorkflowHistory.jsx +++ b/client/src/features/workflows/WorkflowHistory.jsx @@ -42,6 +42,7 @@ const WorkflowHistory = () => { const [error, setError] = useState(null); const [stats, setStats] = useState(null); const [syncingRun, setSyncingRun] = useState(null); + const [deletingRun, setDeletingRun] = useState(null); const calculateStats = (runs) => { if (!runs.length) return null; @@ -143,6 +144,25 @@ const WorkflowHistory = () => { } }; + const handleDeleteRun = async (e, runId) => { + e.stopPropagation(); // Prevent row click + if (!window.confirm('Are you sure you want to delete this workflow run record?')) return; + try { + setDeletingRun(runId); + await apiService.deleteWorkflowRun(runId); + // Remove from local state immediately + setWorkflowRuns(prev => { + const updated = prev.filter(w => w.run.id !== runId); + setStats(calculateStats(updated)); + return updated; + }); + } catch (error) { + console.error('Error deleting workflow run:', error); + } finally { + setDeletingRun(null); + } + }; + const fetchWorkflowHistory = async () => { try { setLoading(true); @@ -192,6 +212,13 @@ const WorkflowHistory = () => { return updated; }); } + }, + onWorkflowDeleted: ({ runId }) => { + setWorkflowRuns(prev => { + const updated = prev.filter(w => w.run.id !== runId); + setStats(calculateStats(updated)); + return updated; + }); } }); @@ -403,7 +430,7 @@ const WorkflowHistory = () => { /> - #{workflow.run.number} + {workflow.run.number ? `#${workflow.run.number}` : '#-'} {workflow.run.head_branch || '-'} @@ -475,6 +502,26 @@ const WorkflowHistory = () => { 'Sync' )} + diff --git a/server/src/controllers/workflowController.js b/server/src/controllers/workflowController.js index 4ebd0af..56eee81 100644 --- a/server/src/controllers/workflowController.js +++ b/server/src/controllers/workflowController.js @@ -255,6 +255,26 @@ export const getWorkflowRunById = async (req, res) => { } }; +export const deleteWorkflowRun = async (req, res) => { + try { + const { id } = req.params; + const workflowRun = await WorkflowRun.findOneAndDelete({ 'run.id': parseInt(id) }); + + if (!workflowRun) { + return errorResponse(res, 'Workflow run not found', 404); + } + + // Notify connected clients so the UI updates in real-time + if (req.io) { + req.io.emit('workflowDeleted', { runId: parseInt(id) }); + } + + return successResponse(res, { deleted: true, runId: parseInt(id) }, 'Workflow run deleted'); + } catch (error) { + return errorResponse(res, 'Error deleting workflow run', 500, error); + } +}; + export const syncWorkflowRun = async (req, res) => { try { const { id } = req.params; diff --git a/server/src/routes/api.js b/server/src/routes/api.js index 8d6e603..7ee23c3 100644 --- a/server/src/routes/api.js +++ b/server/src/routes/api.js @@ -39,6 +39,9 @@ router.get('/workflow-runs/queued', workflowController.getQueuedWorkflows); // Get workflow run by ID router.get('/workflow-runs/:id', workflowController.getWorkflowRunById); +// Delete a workflow run by ID +router.delete('/workflow-runs/:id', workflowController.deleteWorkflowRun); + // Sync workflow run router.post('/workflow-runs/:id/sync', workflowController.syncWorkflowRun); diff --git a/server/src/services/workflowService.js b/server/src/services/workflowService.js index d613d24..f42101d 100644 --- a/server/src/services/workflowService.js +++ b/server/src/services/workflowService.js @@ -53,10 +53,16 @@ export const processWorkflowRun = async (payload) => { status: run.status }); - // Find existing run to preserve any existing labels if none provided + // Find existing run to preserve any existing labels and jobs if none provided const existingRun = await WorkflowRun.findOne({ 'run.id': run.id }); - if (existingRun && !workflowRunData.run.labels.length && existingRun.run.labels?.length) { - workflowRunData.run.labels = existingRun.run.labels; + if (existingRun) { + if (!workflowRunData.run.labels.length && existingRun.run.labels?.length) { + workflowRunData.run.labels = existingRun.run.labels; + } + // Preserve existing jobs — workflow_run events don't carry job data + if (existingRun.jobs?.length) { + workflowRunData.jobs = existingRun.jobs; + } } // Use findOneAndUpdate with upsert to either update existing or create new @@ -312,12 +318,16 @@ export const processWorkflowJobEvent = async (payload) => { return workflowRun; } else { - // If the run doesn't exist, create a new one with the job + // If the run doesn't exist, create a new one with the job. + // Note: workflow_job payloads lack run-level fields (head_branch, event, etc.), + // so we mark this as a partial record. The workflow_run event will fill in the rest. + workflowRunData.run.head_branch = workflow_job.head_branch || null; + workflowRunData.run.event = null; // Not available in workflow_job payload workflowRunData.jobs = [updatedJob]; const workflowRun = await WorkflowRun.findOneAndUpdate( { 'run.id': workflow_job.run_id }, - workflowRunData, + { $setOnInsert: workflowRunData }, { new: true, upsert: true,