diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx index 31012a05a6..4d562738ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx @@ -98,11 +98,7 @@ export function McpDeploy({ const workspaceId = params.workspaceId as string const openSettingsModal = useSettingsModalStore((state) => state.openModal) - const { - data: servers = [], - isLoading: isLoadingServers, - refetch: refetchServers, - } = useWorkflowMcpServers(workspaceId) + const { data: servers = [], isLoading: isLoadingServers } = useWorkflowMcpServers(workspaceId) const addToolMutation = useAddWorkflowMcpTool() const deleteToolMutation = useDeleteWorkflowMcpTool() const updateToolMutation = useUpdateWorkflowMcpTool() @@ -346,7 +342,6 @@ export function McpDeploy({ toolDescription: toolDescription.trim() || undefined, parameterSchema, }) - refetchServers() onAddedToServer?.() logger.info(`Added workflow ${workflowId} as tool to server ${serverId}`) } catch (error) { @@ -375,7 +370,6 @@ export function McpDeploy({ delete next[serverId] return next }) - refetchServers() } catch (error) { logger.error('Failed to remove tool:', error) } finally { @@ -398,7 +392,6 @@ export function McpDeploy({ parameterSchema, addToolMutation, deleteToolMutation, - refetchServers, onAddedToServer, ] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index cb07582380..b3d9cab9d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -52,7 +52,6 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' -import { useChildDeployment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment' import { getAllBlocks } from '@/blocks' import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { @@ -60,7 +59,12 @@ import { useCustomTools, } from '@/hooks/queries/custom-tools' import { useForceRefreshMcpTools, useMcpServers, useStoredMcpTools } from '@/hooks/queries/mcp' -import { useWorkflowInputFields, useWorkflows } from '@/hooks/queries/workflows' +import { + useChildDeploymentStatus, + useDeployChildWorkflow, + useWorkflowInputFields, + useWorkflows, +} from '@/hooks/queries/workflows' import { usePermissionConfig } from '@/hooks/use-permission-config' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' import { useSettingsModalStore } from '@/stores/modals/settings/store' @@ -756,37 +760,26 @@ function WorkflowToolDeployBadge({ workflowId: string onDeploySuccess?: () => void }) { - const { isDeployed, needsRedeploy, isLoading, refetch } = useChildDeployment(workflowId) - const [isDeploying, setIsDeploying] = useState(false) + const { data, isLoading } = useChildDeploymentStatus(workflowId) + const deployMutation = useDeployChildWorkflow() const userPermissions = useUserPermissionsContext() - const deployWorkflow = useCallback(async () => { + const isDeployed = data?.isDeployed ?? null + const needsRedeploy = data?.needsRedeploy ?? false + const isDeploying = deployMutation.isPending + + const deployWorkflow = useCallback(() => { if (isDeploying || !workflowId || !userPermissions.canAdmin) return - try { - setIsDeploying(true) - const response = await fetch(`/api/workflows/${workflowId}/deploy`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + deployMutation.mutate( + { workflowId }, + { + onSuccess: () => { + onDeploySuccess?.() }, - body: JSON.stringify({ - deployChatEnabled: false, - }), - }) - - if (response.ok) { - refetch() - onDeploySuccess?.() - } else { - logger.error('Failed to deploy workflow') } - } catch (error) { - logger.error('Error deploying workflow:', error) - } finally { - setIsDeploying(false) - } - }, [isDeploying, workflowId, refetch, onDeploySuccess, userPermissions.canAdmin]) + ) + }, [isDeploying, workflowId, userPermissions.canAdmin, deployMutation, onDeploySuccess]) if (isLoading || (isDeployed && !needsRedeploy)) { return null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/index.ts index 8a4a62b07f..ccd235eca2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/index.ts @@ -1,5 +1,4 @@ export { useBlockProperties } from './use-block-properties' export { useBlockState } from './use-block-state' export { useChildWorkflow } from './use-child-workflow' -export { useScheduleInfo } from './use-schedule-info' export { useWebhookInfo } from './use-webhook-info' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment.ts deleted file mode 100644 index 666e6705b2..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' - -/** - * Return type for the useChildDeployment hook - */ -export interface UseChildDeploymentReturn { - /** The active version number of the child workflow */ - activeVersion: number | null - /** Whether the child workflow has an active deployment */ - isDeployed: boolean | null - /** Whether the child workflow needs redeployment due to changes */ - needsRedeploy: boolean - /** Whether the deployment information is currently being fetched */ - isLoading: boolean - /** Function to manually refetch deployment status */ - refetch: () => void -} - -/** - * Custom hook for managing child workflow deployment information - * - * @param childWorkflowId - The ID of the child workflow - * @returns Deployment status and version information - */ -export function useChildDeployment(childWorkflowId: string | undefined): UseChildDeploymentReturn { - const [activeVersion, setActiveVersion] = useState(null) - const [isDeployed, setIsDeployed] = useState(null) - const [needsRedeploy, setNeedsRedeploy] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [refetchTrigger, setRefetchTrigger] = useState(0) - - const fetchActiveVersion = useCallback(async (wfId: string) => { - let cancelled = false - - try { - setIsLoading(true) - - const statusRes = await fetch(`/api/workflows/${wfId}/status`, { - cache: 'no-store', - headers: { 'Cache-Control': 'no-cache' }, - }) - - if (!statusRes.ok) { - if (!cancelled) { - setActiveVersion(null) - setIsDeployed(null) - setNeedsRedeploy(false) - } - return - } - - const statusData = await statusRes.json() - - const deploymentsRes = await fetch(`/api/workflows/${wfId}/deployments`, { - cache: 'no-store', - headers: { 'Cache-Control': 'no-cache' }, - }) - - let activeVersion = null - if (deploymentsRes.ok) { - const deploymentsJson = await deploymentsRes.json() - const versions = Array.isArray(deploymentsJson?.data?.versions) - ? deploymentsJson.data.versions - : Array.isArray(deploymentsJson?.versions) - ? deploymentsJson.versions - : [] - - const active = versions.find((v: any) => v.isActive) - activeVersion = active ? Number(active.version) : null - } - - if (!cancelled) { - setActiveVersion(activeVersion) - setIsDeployed(statusData.isDeployed || false) - setNeedsRedeploy(statusData.needsRedeployment || false) - } - } catch { - if (!cancelled) { - setActiveVersion(null) - setIsDeployed(null) - setNeedsRedeploy(false) - } - } finally { - if (!cancelled) setIsLoading(false) - } - - return () => { - cancelled = true - } - }, []) - - useEffect(() => { - if (childWorkflowId) { - void fetchActiveVersion(childWorkflowId) - } else { - setActiveVersion(null) - setIsDeployed(null) - setNeedsRedeploy(false) - } - }, [childWorkflowId, refetchTrigger, fetchActiveVersion]) - - const refetch = useCallback(() => { - setRefetchTrigger((prev) => prev + 1) - }, []) - - return { - activeVersion, - isDeployed, - needsRedeploy, - isLoading, - refetch, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-workflow.ts index 46b4398f87..cf5756f902 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-workflow.ts @@ -1,6 +1,6 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' -import { useChildDeployment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment' import type { WorkflowBlockProps } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/types' +import { useChildDeploymentStatus } from '@/hooks/queries/workflows' /** * Return type for the useChildWorkflow hook @@ -16,12 +16,12 @@ export interface UseChildWorkflowReturn { childNeedsRedeploy: boolean /** Whether the child version information is loading */ isLoadingChildVersion: boolean - /** Function to manually refetch deployment status */ - refetchDeployment: () => void } /** - * Custom hook for managing child workflow information for workflow selector blocks + * Custom hook for managing child workflow information for workflow selector blocks. + * Cache invalidation is handled automatically by React Query when using + * the useDeployChildWorkflow mutation. * * @param blockId - The ID of the block * @param blockType - The type of the block @@ -53,13 +53,14 @@ export function useChildWorkflow( } } - const { - activeVersion: childActiveVersion, - isDeployed: childIsDeployed, - needsRedeploy: childNeedsRedeploy, - isLoading: isLoadingChildVersion, - refetch: refetchDeployment, - } = useChildDeployment(isWorkflowSelector ? childWorkflowId : undefined) + const { data, isLoading, isPending } = useChildDeploymentStatus( + isWorkflowSelector ? childWorkflowId : undefined + ) + + const childActiveVersion = data?.activeVersion ?? null + const childIsDeployed = data?.isDeployed ?? null + const childNeedsRedeploy = data?.needsRedeploy ?? false + const isLoadingChildVersion = isLoading || isPending return { childWorkflowId, @@ -67,6 +68,5 @@ export function useChildWorkflow( childIsDeployed, childNeedsRedeploy, isLoadingChildVersion, - refetchDeployment, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts deleted file mode 100644 index 591c31816d..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback } from 'react' -import { - useReactivateSchedule, - useScheduleInfo as useScheduleInfoQuery, -} from '@/hooks/queries/schedules' -import type { ScheduleInfo } from '../types' - -/** - * Return type for the useScheduleInfo hook - */ -export interface UseScheduleInfoReturn { - /** The schedule configuration and timing information */ - scheduleInfo: ScheduleInfo | null - /** Whether the schedule information is currently being fetched */ - isLoading: boolean - /** Function to reactivate a disabled schedule */ - reactivateSchedule: (scheduleId: string) => Promise -} - -/** - * Custom hook for fetching schedule information using TanStack Query - * - * @param blockId - The ID of the block - * @param blockType - The type of the block - * @param workflowId - The current workflow ID - * @returns Schedule information state and reactivate function - */ -export function useScheduleInfo( - blockId: string, - blockType: string, - workflowId: string -): UseScheduleInfoReturn { - const { scheduleInfo: queryScheduleInfo, isLoading } = useScheduleInfoQuery( - workflowId, - blockId, - blockType - ) - - const reactivateMutation = useReactivateSchedule() - - const reactivateSchedule = useCallback( - async (scheduleId: string) => { - await reactivateMutation.mutateAsync({ - scheduleId, - workflowId, - blockId, - }) - }, - [reactivateMutation, workflowId, blockId] - ) - - const scheduleInfo: ScheduleInfo | null = queryScheduleInfo - ? { - scheduleTiming: queryScheduleInfo.scheduleTiming, - nextRunAt: queryScheduleInfo.nextRunAt, - lastRanAt: queryScheduleInfo.lastRanAt, - timezone: queryScheduleInfo.timezone, - status: queryScheduleInfo.status, - isDisabled: queryScheduleInfo.isDisabled, - failedCount: queryScheduleInfo.failedCount, - id: queryScheduleInfo.id, - } - : null - - return { - scheduleInfo, - isLoading, - reactivateSchedule, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index e81cbdb442..578a3845e5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow' @@ -13,7 +13,6 @@ import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen import { useBlockProperties, useChildWorkflow, - useScheduleInfo, useWebhookInfo, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks' import type { WorkflowBlockProps } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/types' @@ -32,6 +31,8 @@ import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp' import { useCredentialName } from '@/hooks/queries/oauth-credentials' +import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedules' +import { useDeployChildWorkflow } from '@/hooks/queries/workflows' import { useSelectorDisplayName } from '@/hooks/use-selector-display-name' import { useVariablesStore } from '@/stores/panel' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -556,59 +557,32 @@ export const WorkflowBlock = memo(function WorkflowBlock({ reactivateWebhook, } = useWebhookInfo(id, currentWorkflowId) - const { - scheduleInfo, - isLoading: isLoadingScheduleInfo, - reactivateSchedule, - } = useScheduleInfo(id, type, currentWorkflowId) - - const { childWorkflowId, childIsDeployed, childNeedsRedeploy, refetchDeployment } = - useChildWorkflow(id, type, data.isPreview ?? false, data.subBlockValues) - - const [isDeploying, setIsDeploying] = useState(false) - const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus) - - const deployWorkflow = useCallback( - async (workflowId: string) => { - if (isDeploying) return - - try { - setIsDeploying(true) - const response = await fetch(`/api/workflows/${workflowId}/deploy`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - deployChatEnabled: false, - }), - }) - - if (response.ok) { - const responseData = await response.json() - const isDeployedStatus = responseData.isDeployed ?? false - const deployedAtTime = responseData.deployedAt - ? new Date(responseData.deployedAt) - : undefined - setDeploymentStatus( - workflowId, - isDeployedStatus, - deployedAtTime, - responseData.apiKey || '' - ) - refetchDeployment() - } else { - logger.error('Failed to deploy workflow') - } - } catch (error) { - logger.error('Error deploying workflow:', error) - } finally { - setIsDeploying(false) - } + const { scheduleInfo, isLoading: isLoadingScheduleInfo } = useScheduleInfo( + currentWorkflowId, + id, + type + ) + const reactivateScheduleMutation = useReactivateSchedule() + const reactivateSchedule = useCallback( + async (scheduleId: string) => { + await reactivateScheduleMutation.mutateAsync({ + scheduleId, + workflowId: currentWorkflowId, + blockId: id, + }) }, - [isDeploying, setDeploymentStatus, refetchDeployment] + [reactivateScheduleMutation, currentWorkflowId, id] ) + const { childWorkflowId, childIsDeployed, childNeedsRedeploy } = useChildWorkflow( + id, + type, + data.isPreview ?? false, + data.subBlockValues + ) + + const { mutate: deployChildWorkflow, isPending: isDeploying } = useDeployChildWorkflow() + const currentStoreBlock = currentWorkflow.getBlockById(id) const isStarterBlock = type === 'starter' @@ -989,7 +963,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ onClick={(e) => { e.stopPropagation() if (childWorkflowId && !isDeploying && userPermissions.canAdmin) { - deployWorkflow(childWorkflowId) + deployChildWorkflow({ workflowId: childWorkflowId }) } }} > diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index e1e8460e7e..b052f6da30 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -20,6 +20,8 @@ export const workflowKeys = { all: ['workflows'] as const, lists: () => [...workflowKeys.all, 'list'] as const, list: (workspaceId: string | undefined) => [...workflowKeys.lists(), workspaceId ?? ''] as const, + deploymentStatus: (workflowId: string | undefined) => + [...workflowKeys.all, 'deploymentStatus', workflowId ?? ''] as const, deploymentVersions: () => [...workflowKeys.all, 'deploymentVersion'] as const, deploymentVersion: (workflowId: string | undefined, version: number | undefined) => [...workflowKeys.deploymentVersions(), workflowId ?? '', version ?? 0] as const, @@ -517,3 +519,125 @@ export function useReorderWorkflows() { }, }) } + +/** + * Child deployment status data returned from the API + */ +export interface ChildDeploymentStatus { + activeVersion: number | null + isDeployed: boolean + needsRedeploy: boolean +} + +/** + * Fetches deployment status for a child workflow + */ +async function fetchChildDeploymentStatus(workflowId: string): Promise { + const statusRes = await fetch(`/api/workflows/${workflowId}/status`, { + cache: 'no-store', + headers: { 'Cache-Control': 'no-cache' }, + }) + + if (!statusRes.ok) { + throw new Error('Failed to fetch workflow status') + } + + const statusData = await statusRes.json() + + const deploymentsRes = await fetch(`/api/workflows/${workflowId}/deployments`, { + cache: 'no-store', + headers: { 'Cache-Control': 'no-cache' }, + }) + + let activeVersion: number | null = null + if (deploymentsRes.ok) { + const deploymentsJson = await deploymentsRes.json() + const versions = Array.isArray(deploymentsJson?.data?.versions) + ? deploymentsJson.data.versions + : Array.isArray(deploymentsJson?.versions) + ? deploymentsJson.versions + : [] + + const active = versions.find((v: { isActive?: boolean }) => v.isActive) + activeVersion = active ? Number(active.version) : null + } + + return { + activeVersion, + isDeployed: statusData.isDeployed || false, + needsRedeploy: statusData.needsRedeployment || false, + } +} + +/** + * Hook to fetch deployment status for a child workflow. + * Used by workflow selector blocks to show deployment badges. + */ +export function useChildDeploymentStatus(workflowId: string | undefined) { + return useQuery({ + queryKey: workflowKeys.deploymentStatus(workflowId), + queryFn: () => fetchChildDeploymentStatus(workflowId!), + enabled: Boolean(workflowId), + staleTime: 30 * 1000, // 30 seconds + retry: false, + }) +} + +interface DeployChildWorkflowVariables { + workflowId: string +} + +interface DeployChildWorkflowResult { + isDeployed: boolean + deployedAt?: Date + apiKey?: string +} + +/** + * Mutation hook for deploying a child workflow. + * Invalidates the deployment status query on success. + */ +export function useDeployChildWorkflow() { + const queryClient = useQueryClient() + const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus) + + return useMutation({ + mutationFn: async ({ + workflowId, + }: DeployChildWorkflowVariables): Promise => { + const response = await fetch(`/api/workflows/${workflowId}/deploy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + deployChatEnabled: false, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.error || 'Failed to deploy workflow') + } + + const responseData = await response.json() + return { + isDeployed: responseData.isDeployed ?? false, + deployedAt: responseData.deployedAt ? new Date(responseData.deployedAt) : undefined, + apiKey: responseData.apiKey || '', + } + }, + onSuccess: (data, variables) => { + logger.info('Child workflow deployed', { workflowId: variables.workflowId }) + + setDeploymentStatus(variables.workflowId, data.isDeployed, data.deployedAt, data.apiKey || '') + + queryClient.invalidateQueries({ + queryKey: workflowKeys.deploymentStatus(variables.workflowId), + }) + }, + onError: (error) => { + logger.error('Failed to deploy child workflow', { error }) + }, + }) +}