mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(deploy): consolidate deployment detection into single source of truth (#3606)
* fix(deploy): consolidate deployment detection into single source of truth * fix(deploy): address PR review feedback - Remove redundant isLoading check (subset of isPending in RQ v5) - Make deployedState prop nullable to avoid non-null assertion - Destructure mutateAsync to eliminate ESLint suppression - Guard debounced invalidation against stale workflowId on switch * fix(deploy): clean up self-explanatory comments and fix unstable mutation dep - Remove self-explanatory comments in deploy.tsx, tool-input.tsx, use-child-workflow.ts - Tighten non-obvious comments in use-change-detection.ts - Destructure mutate/isPending in WorkflowToolDeployBadge to avoid unstable mutation object in useCallback deps (TanStack Query no-unstable-deps pattern) * lint * fix(deploy): skip expensive state merge when deployedState is null Avoid running mergeSubblockStateWithValues on every render for non-deployed workflows where changeDetected always returns false. * fix(deploy): add missing workflow table import in deploy route Pre-existing type error — workflow table was used but not imported. * fix(deploy): forward AbortSignal in fetchDeployedWorkflowState Match the pattern used by all other fetch helpers in the file so in-flight requests are cancelled on component unmount or query re-trigger. * perf(deploy): parallelize all DB queries in checkNeedsRedeployment All three queries (active version, normalized data, workflow variables) now run concurrently via Promise.all, saving one DB round trip on the common path. * fix(deploy): use sequential-then-parallel pattern in checkNeedsRedeployment * fix(deploy): use client-side comparison for editor header, remove server polling The lastSaved-based server polling was triggering API calls on every local store mutation (before socket persistence), wasting requests and checking stale DB state. Revert the editor header to pure client-side hasWorkflowChanged comparison — zero network during editing, instant badge updates. Child workflow badges still use server-side useDeploymentInfo (they don't have Zustand state). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(deploy): suppress transient Update flash during deployed state refetch Guard change detection on isFetching (not just isLoading) so the comparison is suppressed during background refetches after mutations, preventing a brief Update→Live badge flicker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
680c9cddf0
commit
395a61d1b6
@@ -1,6 +1,6 @@
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
@@ -22,8 +22,11 @@ import {
|
||||
validateWorkflowSchedules,
|
||||
} from '@/lib/workflows/schedules'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import {
|
||||
checkNeedsRedeployment,
|
||||
createErrorResponse,
|
||||
createSuccessResponse,
|
||||
} from '@/app/api/workflows/utils'
|
||||
|
||||
const logger = createLogger('WorkflowDeployAPI')
|
||||
|
||||
@@ -55,43 +58,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
let needsRedeployment = false
|
||||
const [active] = await db
|
||||
.select({ state: workflowDeploymentVersion.state })
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.isActive, true)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(workflowDeploymentVersion.createdAt))
|
||||
.limit(1)
|
||||
|
||||
if (active?.state) {
|
||||
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils')
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(id)
|
||||
if (normalizedData) {
|
||||
const [workflowRecord] = await db
|
||||
.select({ variables: workflow.variables })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
.limit(1)
|
||||
|
||||
const currentState = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
variables: workflowRecord?.variables || {},
|
||||
}
|
||||
const { hasWorkflowChanged } = await import('@/lib/workflows/comparison')
|
||||
needsRedeployment = hasWorkflowChanged(
|
||||
currentState as WorkflowState,
|
||||
active.state as WorkflowState
|
||||
)
|
||||
}
|
||||
}
|
||||
const needsRedeployment = await checkNeedsRedeployment(id)
|
||||
|
||||
logger.info(`[${requestId}] Successfully retrieved deployment info: ${id}`)
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import {
|
||||
checkNeedsRedeployment,
|
||||
createErrorResponse,
|
||||
createSuccessResponse,
|
||||
} from '@/app/api/workflows/utils'
|
||||
|
||||
const logger = createLogger('WorkflowStatusAPI')
|
||||
|
||||
@@ -23,54 +22,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
let needsRedeployment = false
|
||||
|
||||
if (validation.workflow.isDeployed) {
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(id)
|
||||
|
||||
if (!normalizedData) {
|
||||
return createSuccessResponse({
|
||||
isDeployed: validation.workflow.isDeployed,
|
||||
deployedAt: validation.workflow.deployedAt,
|
||||
isPublished: validation.workflow.isPublished,
|
||||
needsRedeployment: false,
|
||||
})
|
||||
}
|
||||
|
||||
const [workflowRecord] = await db
|
||||
.select({ variables: workflow.variables })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
.limit(1)
|
||||
|
||||
const currentState = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
variables: workflowRecord?.variables || {},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
const [active] = await db
|
||||
.select({ state: workflowDeploymentVersion.state })
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.isActive, true)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(workflowDeploymentVersion.createdAt))
|
||||
.limit(1)
|
||||
|
||||
if (active?.state) {
|
||||
needsRedeployment = hasWorkflowChanged(
|
||||
currentState as WorkflowState,
|
||||
active.state as WorkflowState
|
||||
)
|
||||
}
|
||||
}
|
||||
const needsRedeployment = validation.workflow.isDeployed
|
||||
? await checkNeedsRedeployment(id)
|
||||
: false
|
||||
|
||||
return createSuccessResponse({
|
||||
isDeployed: validation.workflow.isDeployed,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('WorkflowUtils')
|
||||
|
||||
@@ -18,6 +23,50 @@ export function createSuccessResponse(data: any) {
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a deployed workflow has changes that require redeployment.
|
||||
* Compares the current persisted state (from normalized tables) against the
|
||||
* active deployment version state.
|
||||
*
|
||||
* This is the single source of truth for redeployment detection — used by
|
||||
* both the /deploy and /status endpoints to ensure consistent results.
|
||||
*/
|
||||
export async function checkNeedsRedeployment(workflowId: string): Promise<boolean> {
|
||||
const [active] = await db
|
||||
.select({ state: workflowDeploymentVersion.state })
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, workflowId),
|
||||
eq(workflowDeploymentVersion.isActive, true)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(workflowDeploymentVersion.createdAt))
|
||||
.limit(1)
|
||||
|
||||
if (!active?.state) return false
|
||||
|
||||
const [normalizedData, [workflowRecord]] = await Promise.all([
|
||||
loadWorkflowFromNormalizedTables(workflowId),
|
||||
db
|
||||
.select({ variables: workflow.variables })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1),
|
||||
])
|
||||
if (!normalizedData) return false
|
||||
|
||||
const currentState = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
variables: workflowRecord?.variables || {},
|
||||
}
|
||||
|
||||
return hasWorkflowChanged(currentState as WorkflowState, active.state as WorkflowState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies user's workspace permissions using the permissions table
|
||||
* @param userId User ID to check
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const logger = createLogger('GeneralDeploy')
|
||||
|
||||
interface GeneralDeployProps {
|
||||
workflowId: string | null
|
||||
deployedState: WorkflowState
|
||||
deployedState?: WorkflowState | null
|
||||
isLoadingDeployedState: boolean
|
||||
versions: WorkflowDeploymentVersionResponse[]
|
||||
versionsLoading: boolean
|
||||
|
||||
+8
-13
@@ -25,7 +25,7 @@ import { startsWithUuid } from '@/executor/constants'
|
||||
import { useA2AAgentByWorkflow } from '@/hooks/queries/a2a/agents'
|
||||
import { useApiKeys } from '@/hooks/queries/api-keys'
|
||||
import {
|
||||
deploymentKeys,
|
||||
invalidateDeploymentQueries,
|
||||
useActivateDeploymentVersion,
|
||||
useChatDeploymentInfo,
|
||||
useDeploymentInfo,
|
||||
@@ -59,9 +59,8 @@ interface DeployModalProps {
|
||||
workflowId: string | null
|
||||
isDeployed: boolean
|
||||
needsRedeployment: boolean
|
||||
deployedState: WorkflowState
|
||||
deployedState?: WorkflowState | null
|
||||
isLoadingDeployedState: boolean
|
||||
refetchDeployedState: () => Promise<void>
|
||||
}
|
||||
|
||||
interface WorkflowDeploymentInfoUI {
|
||||
@@ -84,7 +83,6 @@ export function DeployModal({
|
||||
needsRedeployment,
|
||||
deployedState,
|
||||
isLoadingDeployedState,
|
||||
refetchDeployedState,
|
||||
}: DeployModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const { navigateToSettings } = useSettingsNavigation()
|
||||
@@ -298,17 +296,17 @@ export function DeployModal({
|
||||
setDeployWarnings([])
|
||||
|
||||
try {
|
||||
// Deploy mutation handles query invalidation in its onSuccess callback
|
||||
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
setDeployWarnings(result.warnings)
|
||||
}
|
||||
await refetchDeployedState()
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error deploying workflow:', { error })
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow'
|
||||
setDeployError(errorMessage)
|
||||
}
|
||||
}, [workflowId, deployMutation, refetchDeployedState])
|
||||
}, [workflowId, deployMutation])
|
||||
|
||||
const handlePromoteToLive = useCallback(
|
||||
async (version: number) => {
|
||||
@@ -321,13 +319,12 @@ export function DeployModal({
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
setDeployWarnings(result.warnings)
|
||||
}
|
||||
await refetchDeployedState()
|
||||
} catch (error) {
|
||||
logger.error('Error promoting version:', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[workflowId, activateVersionMutation, refetchDeployedState]
|
||||
[workflowId, activateVersionMutation]
|
||||
)
|
||||
|
||||
const handleUndeploy = useCallback(async () => {
|
||||
@@ -367,13 +364,12 @@ export function DeployModal({
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
setDeployWarnings(result.warnings)
|
||||
}
|
||||
await refetchDeployedState()
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error redeploying workflow:', { error })
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to redeploy workflow'
|
||||
setDeployError(errorMessage)
|
||||
}
|
||||
}, [workflowId, deployMutation, refetchDeployedState])
|
||||
}, [workflowId, deployMutation])
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setChatSubmitting(false)
|
||||
@@ -385,9 +381,8 @@ export function DeployModal({
|
||||
const handleChatDeployed = useCallback(async () => {
|
||||
if (!workflowId) return
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) })
|
||||
invalidateDeploymentQueries(queryClient, workflowId)
|
||||
|
||||
await refetchDeployedState()
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
||||
|
||||
if (chatSuccessTimeoutRef.current) {
|
||||
@@ -395,7 +390,7 @@ export function DeployModal({
|
||||
}
|
||||
setChatSuccess(true)
|
||||
chatSuccessTimeoutRef.current = setTimeout(() => setChatSuccess(false), 2000)
|
||||
}, [workflowId, queryClient, refetchDeployedState])
|
||||
}, [workflowId, queryClient])
|
||||
|
||||
const handleRefetchChat = useCallback(async () => {
|
||||
await refetchChatInfo()
|
||||
|
||||
+10
-20
@@ -6,10 +6,10 @@ import { Button, Tooltip } from '@/components/emcn'
|
||||
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
|
||||
import {
|
||||
useChangeDetection,
|
||||
useDeployedState,
|
||||
useDeployment,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
|
||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
|
||||
import { useDeployedWorkflowState } from '@/hooks/queries/deployments'
|
||||
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
@@ -19,10 +19,6 @@ interface DeployProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy component that handles workflow deployment
|
||||
* Manages deployed state, change detection, and deployment operations
|
||||
*/
|
||||
export function Deploy({ activeWorkflowId, userPermissions, className }: DeployProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
|
||||
@@ -32,30 +28,28 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
||||
hydrationPhase === 'state-loading'
|
||||
const { hasBlocks } = useCurrentWorkflow()
|
||||
|
||||
// Get deployment status from registry
|
||||
const deploymentStatus = useWorkflowRegistry((state) =>
|
||||
state.getWorkflowDeploymentStatus(activeWorkflowId)
|
||||
)
|
||||
const isDeployed = deploymentStatus?.isDeployed || false
|
||||
|
||||
// Fetch and manage deployed state
|
||||
const { deployedState, isLoadingDeployedState, refetchDeployedState } = useDeployedState({
|
||||
workflowId: activeWorkflowId,
|
||||
isDeployed,
|
||||
isRegistryLoading,
|
||||
})
|
||||
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
|
||||
const {
|
||||
data: deployedStateData,
|
||||
isLoading: isLoadingDeployedState,
|
||||
isFetching: isFetchingDeployedState,
|
||||
} = useDeployedWorkflowState(activeWorkflowId, { enabled: isDeployedStateEnabled })
|
||||
const deployedState = isDeployedStateEnabled ? (deployedStateData ?? null) : null
|
||||
|
||||
const { changeDetected } = useChangeDetection({
|
||||
workflowId: activeWorkflowId,
|
||||
deployedState,
|
||||
isLoadingDeployedState,
|
||||
isLoadingDeployedState: isLoadingDeployedState || isFetchingDeployedState,
|
||||
})
|
||||
|
||||
// Handle deployment operations
|
||||
const { isDeploying, handleDeployClick } = useDeployment({
|
||||
workflowId: activeWorkflowId,
|
||||
isDeployed,
|
||||
refetchDeployedState,
|
||||
})
|
||||
|
||||
const isEmpty = !hasBlocks()
|
||||
@@ -71,9 +65,6 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tooltip text based on current state
|
||||
*/
|
||||
const getTooltipText = () => {
|
||||
if (isEmpty) {
|
||||
return 'Cannot deploy an empty workflow'
|
||||
@@ -120,9 +111,8 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
||||
workflowId={activeWorkflowId}
|
||||
isDeployed={isDeployed}
|
||||
needsRedeployment={changeDetected}
|
||||
deployedState={deployedState!}
|
||||
deployedState={deployedState}
|
||||
isLoadingDeployedState={isLoadingDeployedState}
|
||||
refetchDeployedState={refetchDeployedState}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
-1
@@ -1,3 +1,2 @@
|
||||
export { useChangeDetection } from './use-change-detection'
|
||||
export { useDeployedState } from './use-deployed-state'
|
||||
export { useDeployment } from './use-deployment'
|
||||
|
||||
+15
-6
@@ -14,7 +14,9 @@ interface UseChangeDetectionProps {
|
||||
|
||||
/**
|
||||
* Detects meaningful changes between current workflow state and deployed state.
|
||||
* Performs comparison entirely on the client - no API calls needed.
|
||||
* Performs comparison entirely on the client using hasWorkflowChanged — no API
|
||||
* calls needed. The deployed state snapshot is fetched once via React Query and
|
||||
* refreshed after deploy/undeploy/version-activate mutations.
|
||||
*/
|
||||
export function useChangeDetection({
|
||||
workflowId,
|
||||
@@ -41,7 +43,7 @@ export function useChangeDetection({
|
||||
}, [workflowId, allVariables])
|
||||
|
||||
const currentState = useMemo((): WorkflowState | null => {
|
||||
if (!workflowId) return null
|
||||
if (!workflowId || !deployedState) return null
|
||||
|
||||
const mergedBlocks = mergeSubblockStateWithValues(blocks, subBlockValues ?? {})
|
||||
|
||||
@@ -52,12 +54,19 @@ export function useChangeDetection({
|
||||
parallels,
|
||||
variables: workflowVariables,
|
||||
} as WorkflowState & { variables: Record<string, any> }
|
||||
}, [workflowId, blocks, edges, loops, parallels, subBlockValues, workflowVariables])
|
||||
}, [
|
||||
workflowId,
|
||||
deployedState,
|
||||
blocks,
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
subBlockValues,
|
||||
workflowVariables,
|
||||
])
|
||||
|
||||
const changeDetected = useMemo(() => {
|
||||
if (!currentState || !deployedState || isLoadingDeployedState) {
|
||||
return false
|
||||
}
|
||||
if (!currentState || !deployedState || isLoadingDeployedState) return false
|
||||
return hasWorkflowChanged(currentState, deployedState)
|
||||
}, [currentState, deployedState, isLoadingDeployedState])
|
||||
|
||||
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('useDeployedState')
|
||||
|
||||
interface UseDeployedStateProps {
|
||||
workflowId: string | null
|
||||
isDeployed: boolean
|
||||
isRegistryLoading: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and manage deployed workflow state
|
||||
* Includes race condition protection for workflow changes
|
||||
*/
|
||||
export function useDeployedState({
|
||||
workflowId,
|
||||
isDeployed,
|
||||
isRegistryLoading,
|
||||
}: UseDeployedStateProps) {
|
||||
const [deployedState, setDeployedState] = useState<WorkflowState | null>(null)
|
||||
const [isLoadingDeployedState, setIsLoadingDeployedState] = useState<boolean>(false)
|
||||
|
||||
const setNeedsRedeploymentFlag = useWorkflowRegistry(
|
||||
(state) => state.setWorkflowNeedsRedeployment
|
||||
)
|
||||
|
||||
const fetchDeployedState = useCallback(async () => {
|
||||
const registry = useWorkflowRegistry.getState()
|
||||
const currentWorkflowId = registry.activeWorkflowId
|
||||
const deploymentStatus = currentWorkflowId
|
||||
? registry.getWorkflowDeploymentStatus(currentWorkflowId)
|
||||
: null
|
||||
const currentIsDeployed = deploymentStatus?.isDeployed ?? false
|
||||
|
||||
if (!currentWorkflowId || !currentIsDeployed) {
|
||||
setDeployedState(null)
|
||||
return
|
||||
}
|
||||
|
||||
const requestWorkflowId = currentWorkflowId
|
||||
|
||||
try {
|
||||
setIsLoadingDeployedState(true)
|
||||
|
||||
const response = await fetch(`/api/workflows/${requestWorkflowId}/deployed`)
|
||||
|
||||
if (requestWorkflowId !== useWorkflowRegistry.getState().activeWorkflowId) {
|
||||
logger.debug('Workflow changed during deployed state fetch, ignoring response')
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
setDeployedState(null)
|
||||
return
|
||||
}
|
||||
throw new Error(`Failed to fetch deployed state: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
|
||||
setDeployedState(data.deployedState || null)
|
||||
} else {
|
||||
logger.debug('Workflow changed after deployed state response, ignoring result')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching deployed state:', { error })
|
||||
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
|
||||
setDeployedState(null)
|
||||
}
|
||||
} finally {
|
||||
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
|
||||
setIsLoadingDeployedState(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!workflowId) {
|
||||
setDeployedState(null)
|
||||
setIsLoadingDeployedState(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isRegistryLoading) {
|
||||
setDeployedState(null)
|
||||
setIsLoadingDeployedState(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isDeployed) {
|
||||
setNeedsRedeploymentFlag(workflowId, false)
|
||||
fetchDeployedState()
|
||||
} else {
|
||||
setDeployedState(null)
|
||||
setIsLoadingDeployedState(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workflowId, isDeployed, isRegistryLoading, setNeedsRedeploymentFlag])
|
||||
|
||||
return {
|
||||
deployedState,
|
||||
isLoadingDeployedState,
|
||||
refetchDeployedState: fetchDeployedState,
|
||||
}
|
||||
}
|
||||
+10
-53
@@ -1,36 +1,24 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useCallback } from 'react'
|
||||
import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks'
|
||||
import { useDeployWorkflow } from '@/hooks/queries/deployments'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('useDeployment')
|
||||
|
||||
interface UseDeploymentProps {
|
||||
workflowId: string | null
|
||||
isDeployed: boolean
|
||||
refetchDeployedState: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage deployment operations (deploy, undeploy, redeploy)
|
||||
* Hook to manage the deploy button click in the editor header.
|
||||
* First deploy: runs pre-deploy checks, then deploys via mutation and opens modal.
|
||||
* Already deployed: opens modal directly (validation happens on Update in modal).
|
||||
*/
|
||||
export function useDeployment({
|
||||
workflowId,
|
||||
isDeployed,
|
||||
refetchDeployedState,
|
||||
}: UseDeploymentProps) {
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
||||
export function useDeployment({ workflowId, isDeployed }: UseDeploymentProps) {
|
||||
const { mutateAsync, isPending: isDeploying } = useDeployWorkflow()
|
||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||
|
||||
/**
|
||||
* Handle deploy button click
|
||||
* First deploy: calls API to deploy, then opens modal on success
|
||||
* Already deployed: opens modal directly (validation happens on Update in modal)
|
||||
*/
|
||||
const handleDeployClick = useCallback(async () => {
|
||||
if (!workflowId) return { success: false, shouldOpenModal: false }
|
||||
|
||||
@@ -56,39 +44,10 @@ export function useDeployment({
|
||||
return { success: false, shouldOpenModal: false }
|
||||
}
|
||||
|
||||
setIsDeploying(true)
|
||||
try {
|
||||
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 || '')
|
||||
await refetchDeployedState()
|
||||
return { success: true, shouldOpenModal: true }
|
||||
}
|
||||
|
||||
const errorData = await response.json()
|
||||
const errorMessage = errorData.error || 'Failed to deploy workflow'
|
||||
addNotification({
|
||||
level: 'error',
|
||||
message: errorMessage,
|
||||
workflowId,
|
||||
})
|
||||
return { success: false, shouldOpenModal: false }
|
||||
await mutateAsync({ workflowId, deployChatEnabled: false })
|
||||
return { success: true, shouldOpenModal: true }
|
||||
} catch (error) {
|
||||
logger.error('Error deploying workflow:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow'
|
||||
addNotification({
|
||||
level: 'error',
|
||||
@@ -96,10 +55,8 @@ export function useDeployment({
|
||||
workflowId,
|
||||
})
|
||||
return { success: false, shouldOpenModal: false }
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
}, [workflowId, isDeployed, refetchDeployedState, setDeploymentStatus, addNotification])
|
||||
}, [workflowId, isDeployed, addNotification, mutateAsync])
|
||||
|
||||
return {
|
||||
isDeploying,
|
||||
|
||||
+7
-15
@@ -53,18 +53,14 @@ import {
|
||||
type CustomTool as CustomToolDefinition,
|
||||
useCustomTools,
|
||||
} from '@/hooks/queries/custom-tools'
|
||||
import { useDeploymentInfo, useDeployWorkflow } from '@/hooks/queries/deployments'
|
||||
import {
|
||||
useForceRefreshMcpTools,
|
||||
useMcpServers,
|
||||
useMcpToolsEvents,
|
||||
useStoredMcpTools,
|
||||
} from '@/hooks/queries/mcp'
|
||||
import {
|
||||
useChildDeploymentStatus,
|
||||
useDeployChildWorkflow,
|
||||
useWorkflowState,
|
||||
useWorkflows,
|
||||
} from '@/hooks/queries/workflows'
|
||||
import { useWorkflowState, useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
|
||||
@@ -200,9 +196,6 @@ function WorkflowInputMapperInput({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge component showing deployment status for workflow tools
|
||||
*/
|
||||
function WorkflowToolDeployBadge({
|
||||
workflowId,
|
||||
onDeploySuccess,
|
||||
@@ -210,18 +203,17 @@ function WorkflowToolDeployBadge({
|
||||
workflowId: string
|
||||
onDeploySuccess?: () => void
|
||||
}) {
|
||||
const { data, isLoading } = useChildDeploymentStatus(workflowId)
|
||||
const deployMutation = useDeployChildWorkflow()
|
||||
const { data, isLoading } = useDeploymentInfo(workflowId)
|
||||
const { mutate, isPending: isDeploying } = useDeployWorkflow()
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
const isDeployed = data?.isDeployed ?? null
|
||||
const needsRedeploy = data?.needsRedeploy ?? false
|
||||
const isDeploying = deployMutation.isPending
|
||||
const needsRedeploy = data?.needsRedeployment ?? false
|
||||
|
||||
const deployWorkflow = useCallback(() => {
|
||||
if (isDeploying || !workflowId || !userPermissions.canAdmin) return
|
||||
|
||||
deployMutation.mutate(
|
||||
mutate(
|
||||
{ workflowId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -229,7 +221,7 @@ function WorkflowToolDeployBadge({
|
||||
},
|
||||
}
|
||||
)
|
||||
}, [isDeploying, workflowId, userPermissions.canAdmin, deployMutation, onDeploySuccess])
|
||||
}, [isDeploying, workflowId, userPermissions.canAdmin, mutate, onDeploySuccess])
|
||||
|
||||
if (isLoading || (isDeployed && !needsRedeploy)) {
|
||||
return null
|
||||
|
||||
+8
-25
@@ -1,33 +1,18 @@
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import type { WorkflowBlockProps } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/types'
|
||||
import { useChildDeploymentStatus } from '@/hooks/queries/workflows'
|
||||
import { useDeploymentInfo } from '@/hooks/queries/deployments'
|
||||
|
||||
/**
|
||||
* Return type for the useChildWorkflow hook
|
||||
*/
|
||||
export interface UseChildWorkflowReturn {
|
||||
/** The ID of the child workflow if configured */
|
||||
childWorkflowId: string | undefined
|
||||
/** The active version of the child workflow */
|
||||
childActiveVersion: number | null
|
||||
/** Whether the child workflow is deployed */
|
||||
childIsDeployed: boolean | null
|
||||
/** Whether the child workflow needs redeployment due to changes */
|
||||
childNeedsRedeploy: boolean
|
||||
/** Whether the child version information is loading */
|
||||
isLoadingChildVersion: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param isPreview - Whether the block is in preview mode
|
||||
* @param previewSubBlockValues - The subblock values in preview mode
|
||||
* @returns Child workflow configuration and deployment status
|
||||
* Manages child workflow deployment status for workflow selector blocks.
|
||||
* Uses the shared useDeploymentInfo query (same source of truth as the
|
||||
* editor header's Deploy button) for consistent deployment detection.
|
||||
*/
|
||||
export function useChildWorkflow(
|
||||
blockId: string,
|
||||
@@ -53,18 +38,16 @@ export function useChildWorkflow(
|
||||
}
|
||||
}
|
||||
|
||||
const { data, isLoading, isPending } = useChildDeploymentStatus(
|
||||
isWorkflowSelector ? childWorkflowId : undefined
|
||||
const { data, isPending } = useDeploymentInfo(
|
||||
isWorkflowSelector ? (childWorkflowId ?? null) : null
|
||||
)
|
||||
|
||||
const childActiveVersion = data?.activeVersion ?? null
|
||||
const childIsDeployed = data?.isDeployed ?? null
|
||||
const childNeedsRedeploy = data?.needsRedeploy ?? false
|
||||
const isLoadingChildVersion = isLoading || isPending
|
||||
const childNeedsRedeploy = data?.needsRedeployment ?? false
|
||||
const isLoadingChildVersion = isPending
|
||||
|
||||
return {
|
||||
childWorkflowId,
|
||||
childActiveVersion,
|
||||
childIsDeployed,
|
||||
childNeedsRedeploy,
|
||||
isLoadingChildVersion,
|
||||
|
||||
+2
-2
@@ -40,12 +40,12 @@ import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks
|
||||
import { getDependsOnFields } from '@/blocks/utils'
|
||||
import { useKnowledgeBase } from '@/hooks/kb/use-knowledge'
|
||||
import { useCustomTools } from '@/hooks/queries/custom-tools'
|
||||
import { useDeployWorkflow } from '@/hooks/queries/deployments'
|
||||
import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp'
|
||||
import { useCredentialName } from '@/hooks/queries/oauth/oauth-credentials'
|
||||
import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedules'
|
||||
import { useSkills } from '@/hooks/queries/skills'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
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'
|
||||
@@ -918,7 +918,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
data.subBlockValues
|
||||
)
|
||||
|
||||
const { mutate: deployChildWorkflow, isPending: isDeploying } = useDeployChildWorkflow()
|
||||
const { mutate: deployChildWorkflow, isPending: isDeploying } = useDeployWorkflow()
|
||||
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useCallback } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { fetchDeploymentVersionState } from './workflows'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { fetchDeploymentVersionState, workflowKeys } from './workflows'
|
||||
|
||||
const logger = createLogger('DeploymentQueries')
|
||||
|
||||
@@ -14,6 +16,8 @@ export const deploymentKeys = {
|
||||
all: ['deployments'] as const,
|
||||
infos: () => [...deploymentKeys.all, 'info'] as const,
|
||||
info: (workflowId: string | null) => [...deploymentKeys.infos(), workflowId ?? ''] as const,
|
||||
deployedState: (workflowId: string | null) =>
|
||||
[...deploymentKeys.all, 'deployedState', workflowId ?? ''] as const,
|
||||
allVersions: () => [...deploymentKeys.all, 'versions'] as const,
|
||||
versions: (workflowId: string | null) =>
|
||||
[...deploymentKeys.allVersions(), workflowId ?? ''] as const,
|
||||
@@ -29,6 +33,18 @@ export const deploymentKeys = {
|
||||
formDetail: (formId: string | null) => [...deploymentKeys.formDetails(), formId ?? ''] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the core deployment queries (info, deployedState, versions) for a workflow.
|
||||
* Used by mutation onSuccess callbacks and manual invalidation after chat deployments.
|
||||
*/
|
||||
export function invalidateDeploymentQueries(queryClient: QueryClient, workflowId: string) {
|
||||
return Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: deploymentKeys.info(workflowId) }),
|
||||
queryClient.invalidateQueries({ queryKey: deploymentKeys.deployedState(workflowId) }),
|
||||
queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) }),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Response type from /api/workflows/[id]/deploy GET endpoint
|
||||
*/
|
||||
@@ -76,6 +92,40 @@ export function useDeploymentInfo(workflowId: string | null, options?: { enabled
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the deployed workflow state snapshot for a workflow
|
||||
*/
|
||||
async function fetchDeployedWorkflowState(
|
||||
workflowId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkflowState | null> {
|
||||
const response = await fetch(`/api/workflows/${workflowId}/deployed`, { signal })
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) return null
|
||||
throw new Error('Failed to fetch deployed workflow state')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.deployedState || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch the deployed workflow state snapshot.
|
||||
* Returns the full workflow state at the time of the last active deployment.
|
||||
*/
|
||||
export function useDeployedWorkflowState(
|
||||
workflowId: string | null,
|
||||
options?: { enabled?: boolean }
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: deploymentKeys.deployedState(workflowId),
|
||||
queryFn: ({ signal }) => fetchDeployedWorkflowState(workflowId!, signal),
|
||||
enabled: Boolean(workflowId) && (options?.enabled ?? true),
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Response type from /api/workflows/[id]/deployments GET endpoint
|
||||
*/
|
||||
@@ -307,12 +357,12 @@ export function useDeployWorkflow() {
|
||||
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(variables.workflowId, false)
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.info(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.versions(variables.workflowId),
|
||||
})
|
||||
return Promise.all([
|
||||
invalidateDeploymentQueries(queryClient, variables.workflowId),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowKeys.state(variables.workflowId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Failed to deploy workflow', { error })
|
||||
@@ -351,15 +401,12 @@ export function useUndeployWorkflow() {
|
||||
|
||||
setDeploymentStatus(variables.workflowId, false)
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.info(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.versions(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.chatStatus(variables.workflowId),
|
||||
})
|
||||
return Promise.all([
|
||||
invalidateDeploymentQueries(queryClient, variables.workflowId),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.chatStatus(variables.workflowId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Failed to undeploy workflow', { error })
|
||||
@@ -624,12 +671,7 @@ export function useActivateDeploymentVersion() {
|
||||
)
|
||||
},
|
||||
onSettled: (_, __, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.info(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.versions(variables.workflowId),
|
||||
})
|
||||
return invalidateDeploymentQueries(queryClient, variables.workflowId)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ export const workflowKeys = {
|
||||
lists: () => [...workflowKeys.all, 'list'] as const,
|
||||
list: (workspaceId: string | undefined, scope: WorkflowQueryScope = 'active') =>
|
||||
[...workflowKeys.lists(), workspaceId ?? '', scope] 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,
|
||||
@@ -520,10 +518,10 @@ export function useRevertToVersion() {
|
||||
queryKey: workflowKeys.state(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowKeys.deploymentStatus(variables.workflowId),
|
||||
queryKey: deploymentKeys.info(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.info(variables.workflowId),
|
||||
queryKey: deploymentKeys.deployedState(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.versions(variables.workflowId),
|
||||
@@ -590,144 +588,6 @@ 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.
|
||||
* Uses Promise.all to fetch status and deployments in parallel for better performance.
|
||||
*/
|
||||
async function fetchChildDeploymentStatus(
|
||||
workflowId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<ChildDeploymentStatus> {
|
||||
const fetchOptions = {
|
||||
signal,
|
||||
cache: 'no-store' as const,
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
}
|
||||
|
||||
const [statusRes, deploymentsRes] = await Promise.all([
|
||||
fetch(`/api/workflows/${workflowId}/status`, fetchOptions),
|
||||
fetch(`/api/workflows/${workflowId}/deployments`, fetchOptions),
|
||||
])
|
||||
|
||||
if (!statusRes.ok) {
|
||||
throw new Error('Failed to fetch workflow status')
|
||||
}
|
||||
|
||||
const statusData = await statusRes.json()
|
||||
|
||||
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: ({ signal }) => fetchChildDeploymentStatus(workflowId!, signal),
|
||||
enabled: Boolean(workflowId),
|
||||
staleTime: 0,
|
||||
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<DeployChildWorkflowResult> => {
|
||||
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),
|
||||
})
|
||||
// Invalidate workflow state so tool input mappings refresh
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowKeys.state(variables.workflowId),
|
||||
})
|
||||
// Also invalidate deployment queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.info(variables.workflowId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentKeys.versions(variables.workflowId),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Failed to deploy child workflow', { error })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Import workflow mutation (superuser debug)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user