From 1dce08a886e436bc98c53c4353281bd0221a223f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Apr 2026 11:53:34 -0700 Subject: [PATCH 1/3] feat(logs): add cancel execution to log row context menu --- .../log-row-context-menu.tsx | 12 ++++++++++++ .../app/workspace/[workspaceId]/logs/logs.tsx | 13 +++++++++++++ apps/sim/hooks/queries/logs.ts | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+) 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..96bfc7c48b8 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,19 @@ 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') + return res.json() + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: logKeys.all }) + }, + }) +} From 14a802b87a175e39ac6e7e456d84a2701181b353 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Apr 2026 11:59:26 -0700 Subject: [PATCH 2/3] lint --- apps/sim/hooks/queries/logs.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts index 96bfc7c48b8..5948eefb19b 100644 --- a/apps/sim/hooks/queries/logs.ts +++ b/apps/sim/hooks/queries/logs.ts @@ -279,7 +279,13 @@ export function useExecutionSnapshot(executionId: string | undefined) { export function useCancelExecution() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ workflowId, executionId }: { workflowId: string; executionId: string }) => { + mutationFn: async ({ + workflowId, + executionId, + }: { + workflowId: string + executionId: string + }) => { const res = await fetch(`/api/workflows/${workflowId}/executions/${executionId}/cancel`, { method: 'POST', }) From 64424a0617cb232fee128785f16b862492075977 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Apr 2026 12:00:11 -0700 Subject: [PATCH 3/3] fix(logs): check success response and use targeted cache invalidation --- apps/sim/hooks/queries/logs.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts index 5948eefb19b..75ad18c1e50 100644 --- a/apps/sim/hooks/queries/logs.ts +++ b/apps/sim/hooks/queries/logs.ts @@ -290,10 +290,14 @@ export function useCancelExecution() { method: 'POST', }) if (!res.ok) throw new Error('Failed to cancel execution') - return res.json() + const data = await res.json() + if (!data.success) throw new Error('Failed to cancel execution') + return data }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: logKeys.all }) + queryClient.invalidateQueries({ queryKey: logKeys.lists() }) + queryClient.invalidateQueries({ queryKey: logKeys.details() }) + queryClient.invalidateQueries({ queryKey: [...logKeys.all, 'stats'] }) }, }) }