diff --git a/apps/sim/app/api/workflows/[id]/deployed/route.ts b/apps/sim/app/api/workflows/[id]/deployed/route.ts index 8ddf7d1e2e..d0e0d724c2 100644 --- a/apps/sim/app/api/workflows/[id]/deployed/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployed/route.ts @@ -1,5 +1,5 @@ import { eq } from 'drizzle-orm' -import type { NextRequest } from 'next/server' +import type { NextRequest, NextResponse } from 'next/server' import { createLogger } from '@/lib/logs/console-logger' import { db } from '@/db' import { workflow } from '@/db/schema' @@ -11,6 +11,12 @@ const logger = createLogger('WorkflowDeployedStateAPI') export const dynamic = 'force-dynamic' export const runtime = 'nodejs' +// Helper function to add Cache-Control headers to NextResponse +function addNoCacheHeaders(response: NextResponse): NextResponse { + response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + return response +} + export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const requestId = crypto.randomUUID().slice(0, 8) const { id } = await params @@ -21,7 +27,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ if (validation.error) { logger.warn(`[${requestId}] Failed to fetch deployed state: ${validation.error.message}`) - return createErrorResponse(validation.error.message, validation.error.status) + const response = createErrorResponse(validation.error.message, validation.error.status) + return addNoCacheHeaders(response) } // Fetch the workflow's deployed state @@ -36,7 +43,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ if (result.length === 0) { logger.warn(`[${requestId}] Workflow not found: ${id}`) - return createErrorResponse('Workflow not found', 404) + const response = createErrorResponse('Workflow not found', 404) + return addNoCacheHeaders(response) } const workflowData = result[0] @@ -44,18 +52,21 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ // If the workflow is not deployed, return appropriate response if (!workflowData.isDeployed || !workflowData.deployedState) { logger.info(`[${requestId}] No deployed state available for workflow: ${id}`) - return createSuccessResponse({ + const response = createSuccessResponse({ deployedState: null, message: 'Workflow is not deployed or has no deployed state', }) + return addNoCacheHeaders(response) } logger.info(`[${requestId}] Successfully retrieved deployed state for: ${id}`) - return createSuccessResponse({ + const response = createSuccessResponse({ deployedState: workflowData.deployedState, }) + return addNoCacheHeaders(response) } catch (error: any) { logger.error(`[${requestId}] Error fetching deployed state: ${id}`, error) - return createErrorResponse(error.message || 'Failed to fetch deployed state', 500) + const response = createErrorResponse(error.message || 'Failed to fetch deployed state', 500) + return addNoCacheHeaders(response) } } \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx index f395f00ba6..73ad20096a 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -38,6 +38,7 @@ interface DeployModalProps { setNeedsRedeployment: (value: boolean) => void deployedState: any isLoadingDeployedState: boolean + refetchDeployedState: () => Promise } interface ApiKey { @@ -73,6 +74,7 @@ export function DeployModal({ setNeedsRedeployment, deployedState, isLoadingDeployedState, + refetchDeployedState, }: DeployModalProps) { // Store hooks const { addNotification } = useNotificationStore() @@ -310,6 +312,10 @@ export function DeployModal({ setDeploymentInfo(newDeploymentInfo) + // Fetch the updated deployed state after deployment + logger.info('Deployment successful, fetching initial deployed state') + await refetchDeployedState() + // No notification on successful deploy } catch (error: any) { logger.error('Error deploying workflow:', { error }) @@ -399,6 +405,10 @@ export function DeployModal({ useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false) } + // Fetch the updated deployed state after redeployment + logger.info('Redeployment successful, fetching updated deployed state') + await refetchDeployedState() + // Add a success notification addNotification('info', 'Workflow successfully redeployed', workflowId) } catch (error: any) { diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx index 12a0647972..fce1eff9df 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { cn } from '@/lib/utils' -import { WorkflowPreview } from '@/app/w/components/workflow-preview/generic-workflow-preview' +import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview' interface DeployedWorkflowCardProps { currentWorkflowState?: { @@ -27,6 +27,7 @@ export function DeployedWorkflowCard({ }: DeployedWorkflowCardProps) { const [showingDeployed, setShowingDeployed] = useState(true) const workflowToShow = showingDeployed ? deployedWorkflowState : currentWorkflowState + console.log('workflowToShow', workflowToShow) return ( diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx index 0ec6639074..0e6e6fe20e 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx @@ -17,6 +17,7 @@ interface DeploymentControlsProps { setNeedsRedeployment: (value: boolean) => void deployedState: any isLoadingDeployedState: boolean + refetchDeployedState: () => Promise } export function DeploymentControls({ @@ -25,6 +26,7 @@ export function DeploymentControls({ setNeedsRedeployment, deployedState, isLoadingDeployedState, + refetchDeployedState, }: DeploymentControlsProps) { // Use workflow-specific deployment status const deploymentStatus = useWorkflowRegistry((state) => @@ -106,6 +108,7 @@ export function DeploymentControls({ setNeedsRedeployment={setNeedsRedeployment} deployedState={deployedState} isLoadingDeployedState={isLoadingDeployedState} + refetchDeployedState={refetchDeployedState} /> ) diff --git a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx index d844d1c4c9..defb8165e4 100644 --- a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -198,6 +198,260 @@ export function ControlBar() { return () => clearInterval(interval) }, []) + // Listen for workflow changes and check if redeployment is needed + useEffect(() => { + if (!activeWorkflowId || !isDeployed) return + + // Create a debounced function to check for changes + let debounceTimer: NodeJS.Timeout | null = null + let lastCheckTime = 0 + let pendingChanges = 0 + const DEBOUNCE_DELAY = 1000 + const THROTTLE_INTERVAL = 3000 + + // Function to check if redeployment is needed + const checkForChanges = async () => { + // Skip if we're already showing needsRedeployment + if (needsRedeployment) return + + // Reset the pending changes counter + pendingChanges = 0 + lastCheckTime = Date.now() + + try { + // Get the deployed state from the API + const response = await fetch(`/api/workflows/${activeWorkflowId}/status`) + if (response.ok) { + const data = await response.json() + + // If the API says we need redeployment, update our state and the store + if (data.needsRedeployment) { + setNeedsRedeployment(true) + // Also update the store state so other components can access this flag + useWorkflowStore.getState().setNeedsRedeploymentFlag(true) + } + } + } catch (error) { + logger.error('Failed to check workflow change status:', { error }) + } + } + + // Debounced check function + const debouncedCheck = () => { + // Increment the pending changes counter + pendingChanges++ + + // Clear any existing timer + if (debounceTimer) { + clearTimeout(debounceTimer) + } + + // If we recently checked, and it's within throttle interval, wait longer + const timeElapsed = Date.now() - lastCheckTime + if (timeElapsed < THROTTLE_INTERVAL && lastCheckTime > 0) { + // Wait until the throttle interval has passed + const adjustedDelay = Math.max(THROTTLE_INTERVAL - timeElapsed, DEBOUNCE_DELAY) + + debounceTimer = setTimeout(() => { + // Only check if we have pending changes + if (pendingChanges > 0) { + checkForChanges() + } + }, adjustedDelay) + } else { + // Standard debounce delay if we haven't checked recently + debounceTimer = setTimeout(() => { + // Only check if we have pending changes + if (pendingChanges > 0) { + checkForChanges() + } + }, DEBOUNCE_DELAY) + } + } + + // Subscribe to workflow store changes + const workflowUnsubscribe = useWorkflowStore.subscribe(debouncedCheck) + + // Also subscribe to subblock store changes + const subBlockUnsubscribe = useSubBlockStore.subscribe((state) => { + // Only check for the active workflow + if (!activeWorkflowId || !isDeployed || needsRedeployment) return + + // Only trigger when there is an update to the current workflow's subblocks + const workflowSubBlocks = state.workflowValues[activeWorkflowId] + if (workflowSubBlocks && Object.keys(workflowSubBlocks).length > 0) { + debouncedCheck() + } + }) + + return () => { + if (debounceTimer) { + clearTimeout(debounceTimer) + } + workflowUnsubscribe() + subBlockUnsubscribe() + } + }, [activeWorkflowId, isDeployed, needsRedeployment]) + + // Check deployment and publication status on mount or when activeWorkflowId changes + useEffect(() => { + async function checkStatus() { + if (!activeWorkflowId) return + + // Skip API call in localStorage mode + if ( + typeof window !== 'undefined' && + (localStorage.getItem('USE_LOCAL_STORAGE') === 'true' || + process.env.NEXT_PUBLIC_USE_LOCAL_STORAGE === 'true' || + process.env.NEXT_PUBLIC_DISABLE_DB_SYNC === 'true') + ) { + // For localStorage mode, we already have the status in the workflow store + // Nothing more to do as the useWorkflowStore already has this information + return + } + + try { + const response = await fetch(`/api/workflows/${activeWorkflowId}/status`) + if (response.ok) { + const data = await response.json() + // Update the store with the status from the API + setDeploymentStatus( + data.isDeployed, + data.deployedAt ? new Date(data.deployedAt) : undefined + ) + setNeedsRedeployment(data.needsRedeployment) + useWorkflowStore.getState().setNeedsRedeploymentFlag(data.needsRedeployment) + } + } catch (error) { + logger.error('Failed to check workflow status:', { error }) + } + } + checkStatus() + }, [activeWorkflowId, setDeploymentStatus]) + + // Add a function to explicitly fetch deployed state that can be called after redeployment + const refetchDeployedState = async () => { + if (!activeWorkflowId || !isDeployed) { + setDeployedState(null) + return; + } + + try { + setIsLoadingDeployedState(true); + logger.info(`[CENTRAL] Explicitly refetching deployed state for workflow: ${activeWorkflowId}`); + + const response = await fetch(`/api/workflows/${activeWorkflowId}/deployed`); + if (!response.ok) { + throw new Error(`Failed to fetch deployed state: ${response.status}`); + } + + const data = await response.json(); + + if (data.deployedState) { + logger.info('Successfully refetched deployed state from DB after redeployment'); + // Create a deep clone to ensure no reference sharing with current state + const deepClonedState = JSON.parse(JSON.stringify(data.deployedState)); + logger.info('deepClonedState', deepClonedState) + setDeployedState(deepClonedState); + } else { + logger.warn('No deployed state found in the database after refetch'); + setDeployedState(null); + } + } catch (error) { + logger.error('Error refetching deployed state:', error); + setDeployedState(null); + } finally { + setIsLoadingDeployedState(false); + } + }; + + // Fetch deployed state when the workflow ID changes or deployment status changes + useEffect(() => { + async function fetchDeployedState() { + if (!activeWorkflowId || !isDeployed) { + setDeployedState(null) + return + } + + try { + setIsLoadingDeployedState(true) + logger.info(`[CENTRAL] Fetching deployed state for workflow: ${activeWorkflowId} (Control Bar - Single Source of Truth)`) + + const response = await fetch(`/api/workflows/${activeWorkflowId}/deployed`) + if (!response.ok) { + throw new Error(`Failed to fetch deployed state: ${response.status}`) + } + + const data = await response.json() + + if (data.deployedState) { + logger.info('Successfully fetched deployed state from DB - This is the only place that should fetch deployed state') + // Create a deep clone to ensure no reference sharing with current state + const deepClonedState = JSON.parse(JSON.stringify(data.deployedState)) + logger.info('deepClonedState', deepClonedState) + setDeployedState(deepClonedState) + } else { + logger.warn('No deployed state found in the database') + setDeployedState(null) + } + } catch (error) { + logger.error('Error fetching deployed state:', error) + setDeployedState(null) + } finally { + setIsLoadingDeployedState(false) + } + } + + fetchDeployedState() + }, [activeWorkflowId, isDeployed]) + + // Listen for deployment status changes + useEffect(() => { + // When deployment status changes and isDeployed becomes true, + // that means a deployment just occurred, so reset the needsRedeployment flag + if (isDeployed) { + setNeedsRedeployment(false) + useWorkflowStore.getState().setNeedsRedeploymentFlag(false) + + // When a workflow is newly deployed, we need to fetch the deployed state + if (activeWorkflowId && deployedState === null) { + // We'll fetch the deployed state in the other useEffect + } + } else { + // If workflow is undeployed, clear the deployed state + setDeployedState(null) + } + }, [isDeployed, activeWorkflowId]) + + // Listen for deployment status changes + useEffect(() => { + // When deployment status changes and isDeployed becomes true, + // that means a deployment just occurred, so reset the needsRedeployment flag + if (isDeployed) { + setNeedsRedeployment(false) + useWorkflowStore.getState().setNeedsRedeploymentFlag(false) + } + }, [isDeployed]) + + // Add a listener for the needsRedeployment flag in the workflow store + useEffect(() => { + const unsubscribe = useWorkflowStore.subscribe((state) => { + // Update local state when the store flag changes + if (state.needsRedeployment !== undefined) { + setNeedsRedeployment(state.needsRedeployment) + } + }) + + return () => unsubscribe() + }, []) + + // Add a manual method to update the deployment status and clear the needsRedeployment flag + const updateDeploymentStatusAndClearFlag = (isDeployed: boolean, deployedAt?: Date) => { + setDeploymentStatus(isDeployed, deployedAt) + setNeedsRedeployment(false) + useWorkflowStore.getState().setNeedsRedeploymentFlag(false) + } + // Update existing API notifications when needsRedeployment changes useEffect(() => { if (!activeWorkflowId) return @@ -540,6 +794,7 @@ export function ControlBar() { setNeedsRedeployment={setNeedsRedeployment} deployedState={deployedState} isLoadingDeployedState={isLoadingDeployedState} + refetchDeployedState={refetchDeployedState} /> ) @@ -1001,63 +1256,6 @@ export function ControlBar() { ) - // Fetch deployed state when the workflow ID changes or deployment status changes - useEffect(() => { - async function fetchDeployedState() { - if (!activeWorkflowId || !isDeployed) { - setDeployedState(null) - return - } - - try { - setIsLoadingDeployedState(true) - logger.info(`[CENTRAL] Fetching deployed state for workflow: ${activeWorkflowId} (Control Bar - Single Source of Truth)`) - - const response = await fetch(`/api/workflows/${activeWorkflowId}/deployed`) - if (!response.ok) { - throw new Error(`Failed to fetch deployed state: ${response.status}`) - } - - const data = await response.json() - - if (data.deployedState) { - logger.info('Successfully fetched deployed state from DB - This is the only place that should fetch deployed state') - // Create a deep clone to ensure no reference sharing with current state - const deepClonedState = JSON.parse(JSON.stringify(data.deployedState)) - setDeployedState(deepClonedState) - } else { - logger.warn('No deployed state found in the database') - setDeployedState(null) - } - } catch (error) { - logger.error('Error fetching deployed state:', error) - setDeployedState(null) - } finally { - setIsLoadingDeployedState(false) - } - } - - fetchDeployedState() - }, [activeWorkflowId, isDeployed]) - - // Listen for deployment status changes - useEffect(() => { - // When deployment status changes and isDeployed becomes true, - // that means a deployment just occurred, so reset the needsRedeployment flag - if (isDeployed) { - setNeedsRedeployment(false) - useWorkflowStore.getState().setNeedsRedeploymentFlag(false) - - // When a workflow is newly deployed, we need to fetch the deployed state - if (activeWorkflowId && deployedState === null) { - // We'll fetch the deployed state in the other useEffect - } - } else { - // If workflow is undeployed, clear the deployed state - setDeployedState(null) - } - }, [isDeployed, activeWorkflowId]) - return (
{/* Left Section - Workflow Info */} diff --git a/apps/sim/app/w/[id]/components/panel/components/chat/components/output-select/output-select.tsx b/apps/sim/app/w/[id]/components/panel/components/chat/components/output-select/output-select.tsx index b0574274ca..697e038d63 100644 --- a/apps/sim/app/w/[id]/components/panel/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/w/[id]/components/panel/components/chat/components/output-select/output-select.tsx @@ -42,7 +42,10 @@ export function OutputSelect({ // Skip starter/start blocks if (block.type === 'starter') return - const blockName = block.name.replace(/\s+/g, '').toLowerCase() + // Add defensive check to ensure block.name exists and is a string + const blockName = block.name && typeof block.name === 'string' + ? block.name.replace(/\s+/g, '').toLowerCase() + : `block-${block.id}`; // Add response outputs if (block.outputs && typeof block.outputs === 'object') { @@ -60,7 +63,7 @@ export function OutputSelect({ id: `${block.id}_${fullPath}`, label: `${blockName}.${fullPath}`, blockId: block.id, - blockName: block.name, + blockName: block.name || `Block ${block.id}`, blockType: block.type, path: fullPath, }) @@ -93,7 +96,11 @@ export function OutputSelect({ if (validOutputs.length === 1) { const output = workflowOutputs.find((o) => o.id === validOutputs[0]) if (output) { - return `${output.blockName.replace(/\s+/g, '').toLowerCase()}.${output.path}` + // Add defensive check for output.blockName + const blockNameText = output.blockName && typeof output.blockName === 'string' + ? output.blockName.replace(/\s+/g, '').toLowerCase() + : `block-${output.blockId}`; + return `${blockNameText}.${output.path}` } return placeholder } diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index 595ae155d2..faf013deef 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -39,8 +39,6 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { e.stopPropagation() } - const { getValue } = useSubBlockStore() - const isFieldRequired = () => { const blockType = useWorkflowStore.getState().blocks[blockId]?.type if (!blockType) return false diff --git a/apps/sim/app/w/components/workflow-preview/generic-workflow-preview.tsx b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx similarity index 83% rename from apps/sim/app/w/components/workflow-preview/generic-workflow-preview.tsx rename to apps/sim/app/w/components/workflow-preview/workflow-preview.tsx index 68c4cd0e47..0b3b5f0d22 100644 --- a/apps/sim/app/w/components/workflow-preview/generic-workflow-preview.tsx +++ b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx @@ -1,6 +1,6 @@ 'use client' -import { useMemo } from 'react' +import { useMemo, useEffect } from 'react' import ReactFlow, { Background, ConnectionLineType, @@ -27,7 +27,6 @@ import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edg // import { createLoopNode } from '@/app/w/[id]/components/workflow-loop/workflow-loop' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' -import { useEffect } from 'react' const logger = createLogger('WorkflowPreview') @@ -68,7 +67,11 @@ const edgeTypes: EdgeTypes = { workflowEdge: WorkflowEdge, } -function WorkflowPreviewContent({ +// The subblocks should be getting passed from the state and not the subBlockStore. +// Create optional parameter boolan isPreview to pass in the block state to know how to render +// the subblocks + +export function WorkflowPreview({ workflowState, showSubBlocks = true, className, @@ -174,41 +177,35 @@ function WorkflowPreviewContent({ logger.info('Rendering workflow state', { workflowState }) }, [workflowState]) - return ( -
- - - -
- ) -} - -export function WorkflowPreview(props: WorkflowPreviewProps) { return ( - +
+ + + +
) } diff --git a/apps/sim/app/w/marketplace/components/workflow-card.tsx b/apps/sim/app/w/marketplace/components/workflow-card.tsx index 27062916e8..125c52f798 100644 --- a/apps/sim/app/w/marketplace/components/workflow-card.tsx +++ b/apps/sim/app/w/marketplace/components/workflow-card.tsx @@ -4,9 +4,9 @@ import { useEffect, useState } from 'react' import { Eye } from 'lucide-react' import { useRouter } from 'next/navigation' import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card' -import { WorkflowPreview } from '@/app/w/components/workflow-preview/generic-workflow-preview' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import type { Workflow } from '../marketplace' +import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview' +import { Workflow } from '../marketplace' /** * WorkflowCardProps interface - defines the properties for the WorkflowCard component