diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index 0a283a401a2..db260f21ab6 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -22,6 +22,7 @@ interface LogRowContextMenuProps { onOpenPreview: () => void onToggleWorkflowFilter: () => void onClearAllFilters: () => void + onCancelExecution: () => void isFilteredByThisWorkflow: boolean hasActiveFilters: boolean } @@ -41,11 +42,13 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ onOpenPreview, onToggleWorkflowFilter, onClearAllFilters, + onCancelExecution, isFilteredByThisWorkflow, hasActiveFilters, }: LogRowContextMenuProps) { const hasExecutionId = Boolean(log?.executionId) const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId) + const isRunning = log?.status === 'running' && hasExecutionId && hasWorkflow return ( !open && onClose()} modal={false}> @@ -69,6 +72,15 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ sideOffset={4} onCloseAutoFocus={(e) => e.preventDefault()} > + {isRunning && ( + <> + + + Cancel Execution + + + + )} Copy Execution ID diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index f8708263c76..58efb79bcca 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -54,6 +54,7 @@ import { getBlock } from '@/blocks/registry' import { useFolderMap, useFolders } from '@/hooks/queries/folders' import { prefetchLogDetail, + useCancelExecution, useDashboardStats, useLogDetail, useLogsList, @@ -534,6 +535,17 @@ export default function Logs() { } }, [contextMenuLog]) + const cancelExecution = useCancelExecution() + + const handleCancelExecution = useCallback(() => { + const workflowId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId + const executionId = contextMenuLog?.executionId + if (workflowId && executionId) { + cancelExecution.mutate({ workflowId, executionId }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contextMenuLog]) + const contextMenuWorkflowId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId const isFilteredByThisWorkflow = Boolean( contextMenuWorkflowId && workflowIds.length === 1 && workflowIds[0] === contextMenuWorkflowId @@ -1178,6 +1190,7 @@ export default function Logs() { onCopyLink={handleCopyLink} onOpenWorkflow={handleOpenWorkflow} onOpenPreview={handleOpenPreview} + onCancelExecution={handleCancelExecution} onToggleWorkflowFilter={handleToggleWorkflowFilter} onClearAllFilters={handleClearAllFilters} isFilteredByThisWorkflow={isFilteredByThisWorkflow} diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts index 0e684c3dc85..75ad18c1e50 100644 --- a/apps/sim/hooks/queries/logs.ts +++ b/apps/sim/hooks/queries/logs.ts @@ -2,7 +2,9 @@ import { keepPreviousData, type QueryClient, useInfiniteQuery, + useMutation, useQuery, + useQueryClient, } from '@tanstack/react-query' import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters' import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser' @@ -273,3 +275,29 @@ export function useExecutionSnapshot(executionId: string | undefined) { staleTime: 5 * 60 * 1000, // 5 minutes - execution snapshots don't change }) } + +export function useCancelExecution() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async ({ + workflowId, + executionId, + }: { + workflowId: string + executionId: string + }) => { + const res = await fetch(`/api/workflows/${workflowId}/executions/${executionId}/cancel`, { + method: 'POST', + }) + if (!res.ok) throw new Error('Failed to cancel execution') + const data = await res.json() + if (!data.success) throw new Error('Failed to cancel execution') + return data + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: logKeys.lists() }) + queryClient.invalidateQueries({ queryKey: logKeys.details() }) + queryClient.invalidateQueries({ queryKey: [...logKeys.all, 'stats'] }) + }, + }) +}