feat(logs): add cancel execution to log row context menu (#4130)

* feat(logs): add cancel execution to log row context menu

* lint

* fix(logs): check success response and use targeted cache invalidation
This commit is contained in:
Waleed
2026-04-13 12:05:37 -07:00
committed by GitHub
parent cd7e413607
commit eba48e815f
3 changed files with 53 additions and 0 deletions
@@ -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 (
<DropdownMenu open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
@@ -69,6 +72,15 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
sideOffset={4}
onCloseAutoFocus={(e) => e.preventDefault()}
>
{isRunning && (
<>
<DropdownMenuItem onSelect={onCancelExecution} className='text-destructive'>
<X />
Cancel Execution
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem disabled={!hasExecutionId} onSelect={onCopyExecutionId}>
<Copy />
Copy Execution ID
@@ -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}
+28
View File
@@ -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'] })
},
})
}