mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
fix(deployed-workflow): fixed the deployment status changes across workflows. (#382)
* fixed deployment state * fix: moved the deployment status logic to registry store * removed head * fix: changes detected for existing deployment * fix: auto changes detected * fix: rid of debugging * fix: added greptile comments * removed logic for change detection to hook to reduce load on control bar --------- Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local> Co-authored-by: Adam Gough <adamgough@Mac.lan> Co-authored-by: Waleed Latif <walif6@gmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { ChatDeploy } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy'
|
||||
import { DeployForm } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form'
|
||||
@@ -72,7 +73,11 @@ export function DeployModal({
|
||||
}: DeployModalProps) {
|
||||
// Store hooks
|
||||
const { addNotification } = useNotificationStore()
|
||||
const { isDeployed, setDeploymentStatus } = useWorkflowStore()
|
||||
|
||||
// Use registry store for deployment-related functions
|
||||
const deploymentStatus = useWorkflowRegistry(state => state.getWorkflowDeploymentStatus(workflowId))
|
||||
const isDeployed = deploymentStatus?.isDeployed || false
|
||||
const setDeploymentStatus = useWorkflowRegistry(state => state.setDeploymentStatus)
|
||||
|
||||
// Local state
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@@ -80,7 +85,6 @@ export function DeployModal({
|
||||
const [deploymentInfo, setDeploymentInfo] = useState<DeploymentInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
|
||||
const [isCreatingKey, setIsCreatingKey] = useState(false)
|
||||
const [keysLoaded, setKeysLoaded] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<TabView>('api')
|
||||
const [isChatDeploying, setIsChatDeploying] = useState(false)
|
||||
@@ -273,10 +277,13 @@ export function DeployModal({
|
||||
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
|
||||
|
||||
// Update the store with the deployment status
|
||||
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
|
||||
setDeploymentStatus(workflowId, newDeployStatus, deployedAt ? new Date(deployedAt) : undefined, data.apiKey)
|
||||
|
||||
// Reset the needs redeployment flag
|
||||
setNeedsRedeployment(false)
|
||||
if (workflowId) {
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
||||
}
|
||||
|
||||
// Update the local deployment info
|
||||
const endpoint = `${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
|
||||
@@ -322,7 +329,7 @@ export function DeployModal({
|
||||
}
|
||||
|
||||
// Update deployment status in the store
|
||||
setDeploymentStatus(false)
|
||||
setDeploymentStatus(workflowId, false)
|
||||
|
||||
// Reset chat deployment info
|
||||
setDeployedChatUrl(null)
|
||||
@@ -366,13 +373,16 @@ export function DeployModal({
|
||||
throw new Error(errorData.error || 'Failed to redeploy workflow')
|
||||
}
|
||||
|
||||
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
|
||||
const { isDeployed: newDeployStatus, deployedAt, apiKey } = await response.json()
|
||||
|
||||
// Update deployment status in the store
|
||||
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
|
||||
setDeploymentStatus(workflowId, newDeployStatus, deployedAt ? new Date(deployedAt) : undefined, apiKey)
|
||||
|
||||
// Reset the needs redeployment flag
|
||||
setNeedsRedeployment(false)
|
||||
if (workflowId) {
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
||||
}
|
||||
|
||||
// Add a success notification
|
||||
addNotification('info', 'Workflow successfully redeployed', workflowId)
|
||||
@@ -475,10 +485,10 @@ export function DeployModal({
|
||||
throw new Error(errorData.error || 'Failed to deploy workflow')
|
||||
}
|
||||
|
||||
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
|
||||
const { isDeployed: newDeployStatus, deployedAt, apiKey } = await response.json()
|
||||
|
||||
// Update the store with the deployment status
|
||||
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
|
||||
setDeploymentStatus(workflowId, newDeployStatus, deployedAt ? new Date(deployedAt) : undefined, apiKey)
|
||||
|
||||
logger.info('Workflow automatically deployed for chat deployment')
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -7,19 +7,16 @@ import { cn } from '@/lib/utils'
|
||||
import { WorkflowPreview } from '@/app/w/components/workflow-preview/generic-workflow-preview'
|
||||
|
||||
interface DeployedWorkflowCardProps {
|
||||
// Current workflow state (if any)
|
||||
currentWorkflowState?: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
// Deployed workflow state from Supabase
|
||||
deployedWorkflowState: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
// Optional className for styling
|
||||
className?: string
|
||||
}
|
||||
|
||||
@@ -28,15 +25,20 @@ export function DeployedWorkflowCard({
|
||||
deployedWorkflowState,
|
||||
className,
|
||||
}: DeployedWorkflowCardProps) {
|
||||
// State for toggling between deployed and current workflow
|
||||
const [showingDeployed, setShowingDeployed] = useState(true)
|
||||
|
||||
// Determine which workflow state to show
|
||||
const workflowToShow = showingDeployed ? deployedWorkflowState : currentWorkflowState
|
||||
|
||||
return (
|
||||
<Card className={cn('overflow-hidden', className)}>
|
||||
<CardHeader className="space-y-4 p-4">
|
||||
<Card className={cn('overflow-hidden relative', className)}>
|
||||
<CardHeader
|
||||
className={cn(
|
||||
'space-y-4 p-4 sticky top-0 z-10',
|
||||
'backdrop-blur-xl',
|
||||
'bg-background/70 dark:bg-background/50',
|
||||
'border-b border-border/30 dark:border-border/20',
|
||||
'shadow-sm'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">
|
||||
{showingDeployed ? 'Deployed Workflow' : 'Current Workflow'}
|
||||
|
||||
@@ -13,13 +13,7 @@ import {
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -62,8 +56,6 @@ export function DeployedWorkflowModal({
|
||||
<DialogContent
|
||||
className="sm:max-w-[1100px] max-h-[100vh] overflow-y-auto"
|
||||
style={{ zIndex: 1000 }}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
hideCloseButton={true}
|
||||
>
|
||||
<div className="sr-only">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Loader2, Rocket } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { DeployModal } from '../deploy-modal/deploy-modal'
|
||||
|
||||
const logger = createLogger('DeploymentControls')
|
||||
@@ -22,10 +22,27 @@ export function DeploymentControls({
|
||||
needsRedeployment,
|
||||
setNeedsRedeployment,
|
||||
}: DeploymentControlsProps) {
|
||||
const { isDeployed } = useWorkflowStore()
|
||||
// Use workflow-specific deployment status
|
||||
const deploymentStatus = useWorkflowRegistry(state =>
|
||||
state.getWorkflowDeploymentStatus(activeWorkflowId))
|
||||
const isDeployed = deploymentStatus?.isDeployed || false
|
||||
|
||||
// Prioritize workflow-specific needsRedeployment flag, but fall back to prop if needed
|
||||
const workflowNeedsRedeployment = deploymentStatus?.needsRedeployment !== undefined
|
||||
? deploymentStatus.needsRedeployment
|
||||
: needsRedeployment
|
||||
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
// Update parent component when workflow-specific status changes
|
||||
useEffect(() => {
|
||||
if (deploymentStatus?.needsRedeployment !== undefined &&
|
||||
deploymentStatus.needsRedeployment !== needsRedeployment) {
|
||||
setNeedsRedeployment(deploymentStatus.needsRedeployment)
|
||||
}
|
||||
}, [deploymentStatus?.needsRedeployment, needsRedeployment, setNeedsRedeployment, deploymentStatus])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
@@ -46,7 +63,7 @@ export function DeploymentControls({
|
||||
<span className="sr-only">Deploy API</span>
|
||||
</Button>
|
||||
|
||||
{isDeployed && needsRedeployment && (
|
||||
{isDeployed && workflowNeedsRedeployment && (
|
||||
<div className="absolute top-0.5 right-0.5 flex items-center justify-center">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 w-2 h-2 rounded-full bg-amber-500/50 animate-ping"></div>
|
||||
@@ -60,7 +77,7 @@ export function DeploymentControls({
|
||||
<TooltipContent>
|
||||
{isDeploying
|
||||
? 'Deploying...'
|
||||
: isDeployed && needsRedeployment
|
||||
: isDeployed && workflowNeedsRedeployment
|
||||
? 'Workflow changes detected'
|
||||
: isDeployed
|
||||
? 'Deployment Settings'
|
||||
@@ -72,7 +89,7 @@ export function DeploymentControls({
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
workflowId={activeWorkflowId}
|
||||
needsRedeployment={needsRedeployment}
|
||||
needsRedeployment={workflowNeedsRedeployment}
|
||||
setNeedsRedeployment={setNeedsRedeployment}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -8,13 +8,11 @@ import {
|
||||
Bug,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
CreditCard,
|
||||
History,
|
||||
Loader2,
|
||||
Play,
|
||||
SkipForward,
|
||||
StepForward,
|
||||
Store,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
@@ -45,14 +43,13 @@ import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { usePanelStore } from '@/stores/panel/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import { useSidebarStore } from '@/stores/sidebar/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import {
|
||||
getKeyboardShortcutText,
|
||||
useKeyboardShortcuts,
|
||||
} from '../../../hooks/use-keyboard-shortcuts'
|
||||
import { useDeploymentChangeDetection } from '../../hooks/use-deployment-change-detection'
|
||||
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
|
||||
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
|
||||
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
|
||||
@@ -88,10 +85,15 @@ export function ControlBar() {
|
||||
showNotification,
|
||||
removeNotification,
|
||||
} = useNotificationStore()
|
||||
const { history, revertToHistoryState, lastSaved, isDeployed, setDeploymentStatus } =
|
||||
useWorkflowStore()
|
||||
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow, duplicateWorkflow } =
|
||||
useWorkflowRegistry()
|
||||
const { history, revertToHistoryState, lastSaved } = useWorkflowStore()
|
||||
const {
|
||||
workflows,
|
||||
updateWorkflow,
|
||||
activeWorkflowId,
|
||||
removeWorkflow,
|
||||
duplicateWorkflow,
|
||||
setDeploymentStatus,
|
||||
} = useWorkflowRegistry()
|
||||
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
|
||||
const { setActiveTab } = usePanelStore()
|
||||
|
||||
@@ -112,11 +114,6 @@ export function ControlBar() {
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||
|
||||
// Status states
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isPublishing, setIsPublishing] = useState(false)
|
||||
const [needsRedeployment, setNeedsRedeployment] = useState(false)
|
||||
|
||||
// Marketplace modal state
|
||||
const [isMarketplaceModalOpen, setIsMarketplaceModalOpen] = useState(false)
|
||||
|
||||
@@ -153,9 +150,9 @@ export function ControlBar() {
|
||||
)
|
||||
|
||||
// Get notifications for current workflow
|
||||
const workflowNotifications = activeWorkflowId
|
||||
? getWorkflowNotifications(activeWorkflowId)
|
||||
: notifications // Show all if no workflow is active
|
||||
// const workflowNotifications = activeWorkflowId
|
||||
// ? getWorkflowNotifications(activeWorkflowId)
|
||||
// : notifications // Show all if no workflow is active
|
||||
|
||||
// Get the marketplace data from the workflow registry if available
|
||||
const getMarketplaceData = () => {
|
||||
@@ -169,11 +166,21 @@ export function ControlBar() {
|
||||
return !!marketplaceData
|
||||
}
|
||||
|
||||
// Check if the current user is the owner of the published workflow
|
||||
const isWorkflowOwner = () => {
|
||||
const marketplaceData = getMarketplaceData()
|
||||
return marketplaceData?.status === 'owner'
|
||||
}
|
||||
// // Check if the current user is the owner of the published workflow
|
||||
// const isWorkflowOwner = () => {
|
||||
// const marketplaceData = getMarketplaceData()
|
||||
// return marketplaceData?.status === 'owner'
|
||||
// }
|
||||
|
||||
// Get deployment status from registry
|
||||
const deploymentStatus = useWorkflowRegistry((state) =>
|
||||
state.getWorkflowDeploymentStatus(activeWorkflowId)
|
||||
)
|
||||
const isDeployed = deploymentStatus?.isDeployed || false
|
||||
|
||||
// Custom hook for deployment change detection
|
||||
const { needsRedeployment, setNeedsRedeployment, clearNeedsRedeployment } =
|
||||
useDeploymentChangeDetection(activeWorkflowId, isDeployed)
|
||||
|
||||
// Client-side only rendering for the timestamp
|
||||
useEffect(() => {
|
||||
@@ -186,154 +193,6 @@ 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
|
||||
|
||||
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])
|
||||
|
||||
// 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
|
||||
@@ -450,22 +309,22 @@ export function ControlBar() {
|
||||
removeWorkflow(activeWorkflowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening marketplace modal or showing published status
|
||||
*/
|
||||
const handlePublishWorkflow = async () => {
|
||||
if (!activeWorkflowId) return
|
||||
// /**
|
||||
// * Handle opening marketplace modal or showing published status
|
||||
// */
|
||||
// const handlePublishWorkflow = async () => {
|
||||
// if (!activeWorkflowId) return
|
||||
|
||||
// If already published, show marketplace modal with info instead of notifications
|
||||
const isPublished = isPublishedToMarketplace()
|
||||
if (isPublished) {
|
||||
setIsMarketplaceModalOpen(true)
|
||||
return
|
||||
}
|
||||
// // If already published, show marketplace modal with info instead of notifications
|
||||
// const isPublished = isPublishedToMarketplace()
|
||||
// if (isPublished) {
|
||||
// setIsMarketplaceModalOpen(true)
|
||||
// return
|
||||
// }
|
||||
|
||||
// If not published, open the modal to start the publishing process
|
||||
setIsMarketplaceModalOpen(true)
|
||||
}
|
||||
// // If not published, open the modal to start the publishing process
|
||||
// setIsMarketplaceModalOpen(true)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Handle multiple workflow runs
|
||||
@@ -740,7 +599,6 @@ export function ControlBar() {
|
||||
* Render notifications dropdown
|
||||
*/
|
||||
const renderNotificationsDropdown = () => {
|
||||
// Ensure we're only showing notifications for the current workflow
|
||||
const currentWorkflowNotifications = activeWorkflowId
|
||||
? notifications.filter((n) => n.workflowId === activeWorkflowId)
|
||||
: []
|
||||
@@ -845,7 +703,6 @@ export function ControlBar() {
|
||||
* Render debug mode controls
|
||||
*/
|
||||
const renderDebugControls = () => {
|
||||
// Display debug controls only when in debug mode and actively debugging
|
||||
if (!isDebugModeEnabled || !isDebugging) return null
|
||||
|
||||
const pendingCount = pendingBlocks.length
|
||||
@@ -908,9 +765,7 @@ export function ControlBar() {
|
||||
*/
|
||||
const renderDebugModeToggle = () => {
|
||||
const handleToggleDebugMode = () => {
|
||||
// If turning off debug mode, make sure to clean up any debug state
|
||||
if (isDebugModeEnabled) {
|
||||
// Only clean up if we're not actively executing
|
||||
if (!isExecuting) {
|
||||
useExecutionStore.getState().setIsDebugging(false)
|
||||
useExecutionStore.getState().setPendingBlocks([])
|
||||
@@ -942,7 +797,6 @@ export function ControlBar() {
|
||||
|
||||
// Helper function to open subscription settings
|
||||
const openSubscriptionSettings = () => {
|
||||
// Dispatch custom event to open settings modal with subscription tab
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('open-settings', {
|
||||
|
||||
@@ -307,15 +307,17 @@ export function NotificationAlert({ notification, isFading, onHide }: Notificati
|
||||
const { id, type, message, options, workflowId } = notification
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
const [showApiKey, setShowApiKey] = useState(false)
|
||||
const { setDeploymentStatus } = useWorkflowStore()
|
||||
const { isDeployed } = useWorkflowStore((state) => ({
|
||||
isDeployed: state.isDeployed,
|
||||
}))
|
||||
const { setDeploymentStatus } = useWorkflowRegistry()
|
||||
|
||||
// Get deployment status from registry using notification's workflowId, not activeWorkflowId
|
||||
const deploymentStatus = useWorkflowRegistry(state =>
|
||||
state.getWorkflowDeploymentStatus(workflowId || null))
|
||||
const isDeployed = deploymentStatus?.isDeployed || false
|
||||
|
||||
// Create a function to clear the redeployment flag and update deployment status
|
||||
const updateDeploymentStatus = (isDeployed: boolean, deployedAt?: Date) => {
|
||||
// Update deployment status in workflow store
|
||||
setDeploymentStatus(isDeployed, deployedAt)
|
||||
setDeploymentStatus(workflowId || null, isDeployed, deployedAt)
|
||||
|
||||
// Manually update the needsRedeployment flag in workflow store
|
||||
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)
|
||||
|
||||
285
apps/sim/app/w/[id]/hooks/use-deployment-change-detection.ts
Normal file
285
apps/sim/app/w/[id]/hooks/use-deployment-change-detection.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('useDeploymentChangeDetection')
|
||||
|
||||
/**
|
||||
* Hook to detect when a deployed workflow needs redeployment due to changes
|
||||
* Handles debouncing, API checks, and state synchronization
|
||||
*/
|
||||
export function useDeploymentChangeDetection(activeWorkflowId: string | null, isDeployed: boolean) {
|
||||
const [needsRedeployment, setNeedsRedeployment] = useState(false)
|
||||
|
||||
// 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
|
||||
|
||||
// Store the current workflow ID when the effect runs
|
||||
const effectWorkflowId = activeWorkflowId
|
||||
|
||||
// Function to check if redeployment is needed
|
||||
const checkForChanges = async () => {
|
||||
// No longer skip if we're already showing needsRedeployment
|
||||
// This allows us to detect when changes have been reverted
|
||||
|
||||
// Reset the pending changes counter
|
||||
pendingChanges = 0
|
||||
lastCheckTime = Date.now()
|
||||
|
||||
// Store the current workflow ID to check for race conditions
|
||||
const requestedWorkflowId = activeWorkflowId
|
||||
logger.debug(`Checking for changes in workflow ${requestedWorkflowId}`)
|
||||
|
||||
try {
|
||||
// Get the deployed state from the API
|
||||
const response = await fetch(`/api/workflows/${requestedWorkflowId}/status`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
|
||||
// Verify the active workflow hasn't changed while fetching
|
||||
if (requestedWorkflowId !== activeWorkflowId) {
|
||||
logger.debug(
|
||||
`Ignoring changes response for ${requestedWorkflowId} - no longer the active workflow`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`API needsRedeployment response for workflow ${requestedWorkflowId}: ${data.needsRedeployment}`
|
||||
)
|
||||
|
||||
// Always update the needsRedeployment flag based on API response to handle both true and false
|
||||
// This ensures it's updated when changes are detected and when changes are no longer detected
|
||||
if (data.needsRedeployment) {
|
||||
logger.info(
|
||||
`Setting needsRedeployment flag to TRUE for workflow ${requestedWorkflowId}`
|
||||
)
|
||||
|
||||
// Update local state
|
||||
setNeedsRedeployment(true)
|
||||
|
||||
// Use the workflow-specific method to update the registry
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(requestedWorkflowId, true)
|
||||
} else {
|
||||
// Only update to false if the current state is true to avoid unnecessary updates
|
||||
const currentStatus = useWorkflowRegistry
|
||||
.getState()
|
||||
.getWorkflowDeploymentStatus(requestedWorkflowId)
|
||||
if (currentStatus?.needsRedeployment) {
|
||||
logger.info(
|
||||
`Setting needsRedeployment flag to FALSE for workflow ${requestedWorkflowId}`
|
||||
)
|
||||
|
||||
// Update local state
|
||||
setNeedsRedeployment(false)
|
||||
|
||||
// Use the workflow-specific method to update the registry
|
||||
useWorkflowRegistry
|
||||
.getState()
|
||||
.setWorkflowNeedsRedeployment(requestedWorkflowId, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to check workflow change status:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced check function
|
||||
const debouncedCheck = () => {
|
||||
// Skip if the active workflow has changed
|
||||
if (effectWorkflowId !== activeWorkflowId) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 and workflow ID hasn't changed
|
||||
if (pendingChanges > 0 && effectWorkflowId === activeWorkflowId) {
|
||||
checkForChanges()
|
||||
}
|
||||
}, adjustedDelay)
|
||||
} else {
|
||||
// Standard debounce delay if we haven't checked recently
|
||||
debounceTimer = setTimeout(() => {
|
||||
// Only check if we have pending changes and workflow ID hasn't changed
|
||||
if (pendingChanges > 0 && effectWorkflowId === activeWorkflowId) {
|
||||
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 when it's deployed
|
||||
if (!activeWorkflowId || !isDeployed) return
|
||||
|
||||
// Skip if the workflow ID has changed since this effect started
|
||||
if (effectWorkflowId !== activeWorkflowId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only trigger when there is an update to the current workflow's subblocks
|
||||
const workflowSubBlocks = state.workflowValues[effectWorkflowId]
|
||||
if (workflowSubBlocks && Object.keys(workflowSubBlocks).length > 0) {
|
||||
debouncedCheck()
|
||||
}
|
||||
})
|
||||
|
||||
// Set up a periodic check when needsRedeployment is true to ensure it gets set back to false
|
||||
// when changes are reverted
|
||||
let periodicCheckTimer: NodeJS.Timeout | null = null
|
||||
|
||||
if (needsRedeployment) {
|
||||
// Check every 5 seconds when needsRedeployment is true to catch reverted changes
|
||||
const PERIODIC_CHECK_INTERVAL = 5000 // 5 seconds
|
||||
|
||||
periodicCheckTimer = setInterval(() => {
|
||||
// Only perform the check if this is still the active workflow
|
||||
if (effectWorkflowId === activeWorkflowId) {
|
||||
checkForChanges()
|
||||
} else {
|
||||
// Clear the interval if the workflow has changed
|
||||
if (periodicCheckTimer) {
|
||||
clearInterval(periodicCheckTimer)
|
||||
}
|
||||
}
|
||||
}, PERIODIC_CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
if (periodicCheckTimer) {
|
||||
clearInterval(periodicCheckTimer)
|
||||
}
|
||||
workflowUnsubscribe()
|
||||
subBlockUnsubscribe()
|
||||
}
|
||||
}, [activeWorkflowId, isDeployed, needsRedeployment])
|
||||
|
||||
// Initial check on mount or when active workflow changes
|
||||
useEffect(() => {
|
||||
async function checkDeploymentStatus() {
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
try {
|
||||
// Store the current workflow ID to check for race conditions
|
||||
const requestedWorkflowId = activeWorkflowId
|
||||
|
||||
const response = await fetch(`/api/workflows/${requestedWorkflowId}/status`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
|
||||
// Verify the active workflow hasn't changed while fetching
|
||||
if (requestedWorkflowId !== activeWorkflowId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the store with the status from the API
|
||||
useWorkflowRegistry
|
||||
.getState()
|
||||
.setDeploymentStatus(
|
||||
requestedWorkflowId,
|
||||
data.isDeployed,
|
||||
data.deployedAt ? new Date(data.deployedAt) : undefined
|
||||
)
|
||||
|
||||
// Update local state
|
||||
setNeedsRedeployment(data.needsRedeployment)
|
||||
|
||||
// Use the workflow-specific method to update the registry
|
||||
useWorkflowRegistry
|
||||
.getState()
|
||||
.setWorkflowNeedsRedeployment(requestedWorkflowId, data.needsRedeployment)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to check workflow status:', { error })
|
||||
}
|
||||
}
|
||||
checkDeploymentStatus()
|
||||
}, [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) {
|
||||
// Update local state
|
||||
setNeedsRedeployment(false)
|
||||
|
||||
// Use the workflow-specific method to update the registry
|
||||
if (activeWorkflowId) {
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(activeWorkflowId, false)
|
||||
}
|
||||
}
|
||||
}, [isDeployed, activeWorkflowId])
|
||||
|
||||
// Add a listener for the needsRedeployment flag in the workflow store
|
||||
useEffect(() => {
|
||||
const unsubscribe = useWorkflowStore.subscribe((state) => {
|
||||
// Only update local state when it's for the currently active workflow
|
||||
if (state.needsRedeployment !== undefined) {
|
||||
// Get the workflow-specific needsRedeployment flag for the current workflow
|
||||
const currentWorkflowStatus = useWorkflowRegistry
|
||||
.getState()
|
||||
.getWorkflowDeploymentStatus(activeWorkflowId)
|
||||
|
||||
// Only set local state based on current workflow's status
|
||||
if (currentWorkflowStatus?.needsRedeployment !== undefined) {
|
||||
setNeedsRedeployment(currentWorkflowStatus.needsRedeployment)
|
||||
} else {
|
||||
// Fallback to global state only if we don't have workflow-specific status
|
||||
setNeedsRedeployment(state.needsRedeployment)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [activeWorkflowId])
|
||||
|
||||
// Function to clear the redeployment flag
|
||||
const clearNeedsRedeployment = () => {
|
||||
// Update local state
|
||||
setNeedsRedeployment(false)
|
||||
|
||||
// Use the workflow-specific method to update the registry
|
||||
if (activeWorkflowId) {
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(activeWorkflowId, false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
needsRedeployment,
|
||||
setNeedsRedeployment,
|
||||
clearNeedsRedeployment,
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ export function getWorkflowWithValues(workflowId: string) {
|
||||
|
||||
const metadata = workflows[workflowId]
|
||||
|
||||
// Get deployment status from registry
|
||||
const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)
|
||||
|
||||
// Load the specific state for this workflow
|
||||
let workflowState: WorkflowState
|
||||
|
||||
@@ -34,8 +37,8 @@ export function getWorkflowWithValues(workflowId: string) {
|
||||
blocks: currentState.blocks,
|
||||
edges: currentState.edges,
|
||||
loops: currentState.loops,
|
||||
isDeployed: currentState.isDeployed,
|
||||
deployedAt: currentState.deployedAt,
|
||||
isDeployed: deploymentStatus?.isDeployed || false,
|
||||
deployedAt: deploymentStatus?.deployedAt,
|
||||
lastSaved: currentState.lastSaved,
|
||||
}
|
||||
} else {
|
||||
@@ -45,7 +48,13 @@ export function getWorkflowWithValues(workflowId: string) {
|
||||
logger.warn(`No saved state found for workflow ${workflowId}`)
|
||||
return null
|
||||
}
|
||||
workflowState = savedState
|
||||
|
||||
// Use registry deployment status instead of relying on saved state
|
||||
workflowState = {
|
||||
...savedState,
|
||||
isDeployed: deploymentStatus?.isDeployed || savedState.isDeployed || false,
|
||||
deployedAt: deploymentStatus?.deployedAt || savedState.deployedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subblock values for this specific workflow
|
||||
@@ -103,6 +112,9 @@ export function getAllWorkflowsWithValues() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get deployment status from registry
|
||||
const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(id)
|
||||
|
||||
// Load the specific state for this workflow
|
||||
let workflowState: WorkflowState
|
||||
|
||||
@@ -112,8 +124,8 @@ export function getAllWorkflowsWithValues() {
|
||||
blocks: currentState.blocks,
|
||||
edges: currentState.edges,
|
||||
loops: currentState.loops,
|
||||
isDeployed: currentState.isDeployed,
|
||||
deployedAt: currentState.deployedAt,
|
||||
isDeployed: deploymentStatus?.isDeployed || false,
|
||||
deployedAt: deploymentStatus?.deployedAt,
|
||||
lastSaved: currentState.lastSaved,
|
||||
}
|
||||
} else {
|
||||
@@ -124,12 +136,21 @@ export function getAllWorkflowsWithValues() {
|
||||
logger.warn(`No saved state found for workflow ${id}`)
|
||||
continue
|
||||
}
|
||||
workflowState = savedState
|
||||
|
||||
// Use registry deployment status instead of relying on saved state
|
||||
workflowState = {
|
||||
...savedState,
|
||||
isDeployed: deploymentStatus?.isDeployed || savedState.isDeployed || false,
|
||||
deployedAt: deploymentStatus?.deployedAt || savedState.deployedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subblock values for this specific workflow
|
||||
const mergedBlocks = mergeSubblockState(workflowState.blocks, id)
|
||||
|
||||
// Include the API key in the state if it exists in the deployment status
|
||||
const apiKey = deploymentStatus?.apiKey
|
||||
|
||||
result[id] = {
|
||||
id,
|
||||
name: metadata.name,
|
||||
@@ -145,6 +166,8 @@ export function getAllWorkflowsWithValues() {
|
||||
isDeployed: workflowState.isDeployed,
|
||||
deployedAt: workflowState.deployedAt,
|
||||
},
|
||||
// Include API key if available
|
||||
apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
workflowSync,
|
||||
} from '../sync'
|
||||
import { useWorkflowStore } from '../workflow/store'
|
||||
import { WorkflowMetadata, WorkflowRegistry } from './types'
|
||||
import { DeploymentStatus, WorkflowMetadata, WorkflowRegistry } from './types'
|
||||
import { generateUniqueName, getNextWorkflowColor } from './utils'
|
||||
|
||||
const logger = createLogger('WorkflowRegistry')
|
||||
@@ -69,7 +69,6 @@ function cleanupLocalStorageForWorkspace(workspaceId: string): void {
|
||||
// Only remove if it belongs to the current workspace
|
||||
if (parsed.workspaceId === workspaceId) {
|
||||
localStorage.removeItem(key)
|
||||
logger.debug(`Removed stale localStorage data for workflow ${workflowId}`)
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip if we can't parse the data
|
||||
@@ -78,7 +77,6 @@ function cleanupLocalStorageForWorkspace(workspaceId: string): void {
|
||||
} else {
|
||||
// If we can't determine the workspace, remove it to be safe
|
||||
localStorage.removeItem(key)
|
||||
logger.debug(`Removed stale localStorage data for workflow ${workflowId}`)
|
||||
}
|
||||
}
|
||||
// Case 2: Clean up workflows that reference deleted workspaces
|
||||
@@ -99,9 +97,6 @@ function cleanupLocalStorageForWorkspace(workspaceId: string): void {
|
||||
// Workspace doesn't exist, update the workflow to use current workspace
|
||||
parsed.workspaceId = workspaceId
|
||||
localStorage.setItem(`workflow-${workflowId}`, JSON.stringify(parsed))
|
||||
logger.debug(
|
||||
`Updated workflow ${workflowId} to use current workspace ${workspaceId}`
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip if we can't parse workspaces data
|
||||
@@ -132,6 +127,7 @@ function resetWorkflowStores() {
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {}, // Reset deployment statuses map
|
||||
hasActiveSchedule: false,
|
||||
history: {
|
||||
past: [],
|
||||
@@ -195,6 +191,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
typeof window !== 'undefined' ? localStorage.getItem(ACTIVE_WORKSPACE_KEY) : null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
// Initialize deployment statuses
|
||||
deploymentStatuses: {},
|
||||
|
||||
// Set loading state
|
||||
setLoading: (loading: boolean) => {
|
||||
@@ -322,7 +320,173 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
})
|
||||
},
|
||||
|
||||
// Switch to a different workflow and manage state persistence
|
||||
// Method to get deployment status for a specific workflow
|
||||
getWorkflowDeploymentStatus: (workflowId: string | null): DeploymentStatus | null => {
|
||||
if (!workflowId) {
|
||||
// If no workflow ID provided, check the active workflow
|
||||
workflowId = get().activeWorkflowId
|
||||
if (!workflowId) return null
|
||||
}
|
||||
|
||||
const { deploymentStatuses = {} } = get()
|
||||
|
||||
// First try to get from the workflow-specific deployment statuses
|
||||
if (deploymentStatuses[workflowId]) {
|
||||
return deploymentStatuses[workflowId]
|
||||
}
|
||||
|
||||
// For backward compatibility, check the workflow state in workflow store
|
||||
// This will only be relevant during the transition period
|
||||
const workflowState = loadWorkflowState(workflowId)
|
||||
if (workflowState) {
|
||||
// Check workflow-specific status in the workflow state
|
||||
if (workflowState.deploymentStatuses?.[workflowId]) {
|
||||
return workflowState.deploymentStatuses[workflowId]
|
||||
}
|
||||
|
||||
// Fallback to legacy fields if needed
|
||||
if (workflowState.isDeployed) {
|
||||
return {
|
||||
isDeployed: workflowState.isDeployed || false,
|
||||
deployedAt: workflowState.deployedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No deployment status found
|
||||
return null
|
||||
},
|
||||
|
||||
// Method to set deployment status for a specific workflow
|
||||
setDeploymentStatus: (
|
||||
workflowId: string | null,
|
||||
isDeployed: boolean,
|
||||
deployedAt?: Date,
|
||||
apiKey?: string
|
||||
) => {
|
||||
if (!workflowId) {
|
||||
workflowId = get().activeWorkflowId
|
||||
if (!workflowId) return
|
||||
}
|
||||
|
||||
// Update the deployment statuses in the registry
|
||||
set((state) => ({
|
||||
deploymentStatuses: {
|
||||
...state.deploymentStatuses,
|
||||
[workflowId as string]: {
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
apiKey,
|
||||
// Preserve existing needsRedeployment flag if available, but reset if newly deployed
|
||||
needsRedeployment: isDeployed
|
||||
? false
|
||||
: ((state.deploymentStatuses?.[workflowId as string] as any)?.needsRedeployment ??
|
||||
false),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Also update the workflow store if this is the active workflow
|
||||
const { activeWorkflowId } = get()
|
||||
if (workflowId === activeWorkflowId) {
|
||||
// Update the workflow store for backward compatibility
|
||||
useWorkflowStore.setState((state) => ({
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
needsRedeployment: isDeployed ? false : state.needsRedeployment,
|
||||
deploymentStatuses: {
|
||||
...state.deploymentStatuses,
|
||||
[workflowId as string]: {
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
apiKey,
|
||||
needsRedeployment: isDeployed
|
||||
? false
|
||||
: ((state.deploymentStatuses?.[workflowId as string] as any)?.needsRedeployment ??
|
||||
false),
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// Save the deployment status in the workflow state
|
||||
const workflowState = loadWorkflowState(workflowId)
|
||||
if (workflowState) {
|
||||
saveWorkflowState(workflowId, {
|
||||
...workflowState,
|
||||
// Update both legacy and new fields for compatibility
|
||||
isDeployed: workflowId === activeWorkflowId ? isDeployed : workflowState.isDeployed,
|
||||
deployedAt:
|
||||
workflowId === activeWorkflowId
|
||||
? deployedAt || (isDeployed ? new Date() : undefined)
|
||||
: workflowState.deployedAt,
|
||||
deploymentStatuses: {
|
||||
...(workflowState.deploymentStatuses || {}),
|
||||
[workflowId]: {
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
apiKey,
|
||||
needsRedeployment: isDeployed
|
||||
? false
|
||||
: ((workflowState.deploymentStatuses?.[workflowId] as any)?.needsRedeployment ??
|
||||
false),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Trigger workflow sync to update server state
|
||||
workflowSync.sync()
|
||||
},
|
||||
|
||||
// Method to set the needsRedeployment flag for a specific workflow
|
||||
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => {
|
||||
if (!workflowId) {
|
||||
workflowId = get().activeWorkflowId
|
||||
if (!workflowId) return
|
||||
}
|
||||
|
||||
// Update the registry's deployment status for this specific workflow
|
||||
set((state) => {
|
||||
const deploymentStatuses = state.deploymentStatuses || {}
|
||||
const currentStatus = deploymentStatuses[workflowId as string] || { isDeployed: false }
|
||||
|
||||
return {
|
||||
deploymentStatuses: {
|
||||
...deploymentStatuses,
|
||||
[workflowId as string]: {
|
||||
...currentStatus,
|
||||
needsRedeployment,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Only update the global flag if this is the active workflow
|
||||
const { activeWorkflowId } = get()
|
||||
if (workflowId === activeWorkflowId) {
|
||||
useWorkflowStore.getState().setNeedsRedeploymentFlag(needsRedeployment)
|
||||
}
|
||||
// Save to persistent storage
|
||||
const workflowState = loadWorkflowState(workflowId)
|
||||
if (workflowState) {
|
||||
const deploymentStatuses = workflowState.deploymentStatuses || {}
|
||||
const currentStatus = deploymentStatuses[workflowId] || { isDeployed: false }
|
||||
|
||||
saveWorkflowState(workflowId, {
|
||||
...workflowState,
|
||||
deploymentStatuses: {
|
||||
...deploymentStatuses,
|
||||
[workflowId]: {
|
||||
...currentStatus,
|
||||
needsRedeployment,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Modified setActiveWorkflow to load deployment statuses
|
||||
setActiveWorkflow: async (id: string) => {
|
||||
const { workflows } = get()
|
||||
if (!workflows[id]) {
|
||||
@@ -330,8 +494,9 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
return
|
||||
}
|
||||
|
||||
// Save current workflow state before switching
|
||||
// Get current workflow ID
|
||||
const currentId = get().activeWorkflowId
|
||||
// Save current workflow state before switching
|
||||
if (currentId) {
|
||||
const currentState = useWorkflowStore.getState()
|
||||
|
||||
@@ -343,6 +508,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
history: currentState.history,
|
||||
isDeployed: currentState.isDeployed,
|
||||
deployedAt: currentState.deployedAt,
|
||||
deploymentStatuses: currentState.deploymentStatuses,
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
@@ -356,7 +522,28 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
// Load workflow state for the new active workflow
|
||||
const parsedState = loadWorkflowState(id)
|
||||
if (parsedState) {
|
||||
const { blocks, edges, history, loops, isDeployed, deployedAt } = parsedState
|
||||
const {
|
||||
blocks,
|
||||
edges,
|
||||
history,
|
||||
loops,
|
||||
isDeployed,
|
||||
deployedAt,
|
||||
deploymentStatuses,
|
||||
needsRedeployment,
|
||||
} = parsedState
|
||||
|
||||
// Get workflow-specific deployment status
|
||||
let workflowIsDeployed = isDeployed
|
||||
let workflowDeployedAt = deployedAt
|
||||
let workflowNeedsRedeployment = needsRedeployment
|
||||
|
||||
// Check if we have a workflow-specific deployment status
|
||||
if (deploymentStatuses && deploymentStatuses[id]) {
|
||||
workflowIsDeployed = deploymentStatuses[id].isDeployed
|
||||
workflowDeployedAt = deploymentStatuses[id].deployedAt
|
||||
workflowNeedsRedeployment = deploymentStatuses[id].needsRedeployment
|
||||
}
|
||||
|
||||
// Initialize subblock store with workflow values
|
||||
useSubBlockStore.getState().initializeFromWorkflow(id, blocks)
|
||||
@@ -366,8 +553,11 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
blocks,
|
||||
edges,
|
||||
loops,
|
||||
isDeployed: isDeployed !== undefined ? isDeployed : false,
|
||||
deployedAt: deployedAt ? new Date(deployedAt) : undefined,
|
||||
isDeployed: workflowIsDeployed !== undefined ? workflowIsDeployed : false,
|
||||
deployedAt: workflowDeployedAt ? new Date(workflowDeployedAt) : undefined,
|
||||
needsRedeployment:
|
||||
workflowNeedsRedeployment !== undefined ? workflowNeedsRedeployment : false,
|
||||
deploymentStatuses: deploymentStatuses || {},
|
||||
hasActiveSchedule: false,
|
||||
history: history || {
|
||||
past: [],
|
||||
@@ -376,8 +566,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
blocks,
|
||||
edges,
|
||||
loops: {},
|
||||
isDeployed: isDeployed !== undefined ? isDeployed : false,
|
||||
deployedAt: deployedAt,
|
||||
isDeployed: workflowIsDeployed !== undefined ? workflowIsDeployed : false,
|
||||
deployedAt: workflowDeployedAt,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
@@ -387,6 +577,30 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
},
|
||||
lastSaved: parsedState.lastSaved || Date.now(),
|
||||
})
|
||||
|
||||
// Update the deployment statuses in the registry
|
||||
if (deploymentStatuses) {
|
||||
set((state) => ({
|
||||
deploymentStatuses: {
|
||||
...state.deploymentStatuses,
|
||||
...deploymentStatuses,
|
||||
},
|
||||
}))
|
||||
} else if (workflowIsDeployed !== undefined) {
|
||||
// If there's no deployment statuses object but we have legacy deployment status,
|
||||
// create an entry in the deploymentStatuses map
|
||||
set((state) => ({
|
||||
deploymentStatuses: {
|
||||
...state.deploymentStatuses,
|
||||
[id]: {
|
||||
isDeployed: workflowIsDeployed as boolean,
|
||||
deployedAt: workflowDeployedAt ? new Date(workflowDeployedAt) : undefined,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
logger.info(`Switched to workflow ${id}`)
|
||||
} else {
|
||||
// If no saved state, initialize with empty state
|
||||
useWorkflowStore.setState({
|
||||
@@ -395,6 +609,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {},
|
||||
hasActiveSchedule: false,
|
||||
history: {
|
||||
past: [],
|
||||
@@ -459,6 +674,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
loops: options.marketplaceState.loops || {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {}, // Initialize empty deployment statuses map
|
||||
workspaceId, // Include workspace ID in the state object
|
||||
history: {
|
||||
past: [],
|
||||
@@ -582,6 +798,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {}, // Initialize empty deployment statuses map
|
||||
workspaceId, // Include workspace ID in the state object
|
||||
history: {
|
||||
past: [],
|
||||
@@ -776,6 +993,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
isDeployed: false, // Reset deployment status
|
||||
deployedAt: undefined, // Reset deployment timestamp
|
||||
workspaceId, // Include workspaceId in state
|
||||
deploymentStatuses: {}, // Start with empty deployment statuses map
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
|
||||
@@ -3,6 +3,13 @@ export interface MarketplaceData {
|
||||
status: 'owner' | 'temp'
|
||||
}
|
||||
|
||||
export interface DeploymentStatus {
|
||||
isDeployed: boolean
|
||||
deployedAt?: Date
|
||||
apiKey?: string
|
||||
needsRedeployment?: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowMetadata {
|
||||
id: string
|
||||
name: string
|
||||
@@ -19,6 +26,7 @@ export interface WorkflowRegistryState {
|
||||
activeWorkspaceId: string | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
deploymentStatuses: Record<string, DeploymentStatus>
|
||||
}
|
||||
|
||||
export interface WorkflowRegistryActions {
|
||||
@@ -37,6 +45,14 @@ export interface WorkflowRegistryActions {
|
||||
workspaceId?: string
|
||||
}) => string
|
||||
duplicateWorkflow: (sourceId: string) => string | null
|
||||
getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null
|
||||
setDeploymentStatus: (
|
||||
workflowId: string | null,
|
||||
isDeployed: boolean,
|
||||
deployedAt?: Date,
|
||||
apiKey?: string
|
||||
) => void
|
||||
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => void
|
||||
}
|
||||
|
||||
export type WorkflowRegistry = WorkflowRegistryState & WorkflowRegistryActions
|
||||
|
||||
@@ -3,6 +3,7 @@ import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { pushHistory, withHistory, WorkflowStoreWithHistory } from '../middleware'
|
||||
import { saveWorkflowState } from '../persistence'
|
||||
import { useWorkflowRegistry } from '../registry/store'
|
||||
@@ -12,13 +13,18 @@ import { mergeSubblockState } from '../utils'
|
||||
import { Loop, Position, SubBlockState, SyncControl, WorkflowState } from './types'
|
||||
import { detectCycle } from './utils'
|
||||
|
||||
const logger = createLogger('WorkflowStore')
|
||||
|
||||
const initialState = {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
lastSaved: undefined,
|
||||
// Legacy deployment fields (keeping for compatibility but they will be deprecated)
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
// New field for per-workflow deployment tracking
|
||||
deploymentStatuses: {},
|
||||
needsRedeployment: false,
|
||||
hasActiveSchedule: false,
|
||||
hasActiveWebhook: false,
|
||||
@@ -382,8 +388,10 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: currentState.edges,
|
||||
loops: currentState.loops,
|
||||
history: currentState.history,
|
||||
// Include both legacy and new deployment status fields
|
||||
isDeployed: currentState.isDeployed,
|
||||
deployedAt: currentState.deployedAt,
|
||||
deploymentStatuses: currentState.deploymentStatuses,
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
@@ -683,20 +691,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}))
|
||||
},
|
||||
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => {
|
||||
const newState = {
|
||||
...get(),
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
needsRedeployment: isDeployed ? false : get().needsRedeployment,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
get().updateLastSaved()
|
||||
get().sync.markDirty()
|
||||
get().sync.forceSync()
|
||||
},
|
||||
|
||||
setScheduleStatus: (hasActiveSchedule: boolean) => {
|
||||
// Only update if the status has changed to avoid unnecessary rerenders
|
||||
if (get().hasActiveSchedule !== hasActiveSchedule) {
|
||||
@@ -721,20 +715,34 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
|
||||
revertToDeployedState: (deployedState: WorkflowState) => {
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
|
||||
// Preserving the workflow-specific deployment status if it exists
|
||||
const deploymentStatus = activeWorkflowId
|
||||
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(activeWorkflowId)
|
||||
: null;
|
||||
|
||||
const newState = {
|
||||
blocks: deployedState.blocks,
|
||||
edges: deployedState.edges,
|
||||
loops: deployedState.loops,
|
||||
// Legacy fields for backward compatibility
|
||||
isDeployed: true,
|
||||
needsRedeployment: false,
|
||||
hasActiveWebhook: false, // Reset webhook status
|
||||
// Keep existing deployment statuses and update for the active workflow if needed
|
||||
deploymentStatuses: {
|
||||
...get().deploymentStatuses,
|
||||
...(activeWorkflowId && deploymentStatus ? {
|
||||
[activeWorkflowId]: deploymentStatus
|
||||
} : {})
|
||||
}
|
||||
}
|
||||
|
||||
// Update the main workflow state
|
||||
set(newState)
|
||||
|
||||
// Get the active workflow ID
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Initialize subblock store with values from deployed state
|
||||
|
||||
@@ -34,14 +34,24 @@ export interface Loop {
|
||||
forEachItems?: any[] | Record<string, any> | string // Items or expression
|
||||
}
|
||||
|
||||
export interface DeploymentStatus {
|
||||
isDeployed: boolean
|
||||
deployedAt?: Date
|
||||
apiKey?: string
|
||||
needsRedeployment?: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowState {
|
||||
blocks: Record<string, BlockState>
|
||||
edges: Edge[]
|
||||
lastSaved?: number
|
||||
loops: Record<string, Loop>
|
||||
lastUpdate?: number
|
||||
// Legacy deployment fields (keeping for compatibility)
|
||||
isDeployed?: boolean
|
||||
deployedAt?: Date
|
||||
// New field for per-workflow deployment status
|
||||
deploymentStatuses?: Record<string, DeploymentStatus>
|
||||
needsRedeployment?: boolean
|
||||
hasActiveSchedule?: boolean
|
||||
hasActiveWebhook?: boolean
|
||||
@@ -76,7 +86,6 @@ export interface WorkflowActions {
|
||||
updateLoopType: (loopId: string, loopType: Loop['loopType']) => void
|
||||
updateLoopForEachItems: (loopId: string, items: string) => void
|
||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
|
||||
setScheduleStatus: (hasActiveSchedule: boolean) => void
|
||||
setWebhookStatus: (hasActiveWebhook: boolean) => void
|
||||
toggleBlockAdvancedMode: (id: string) => void
|
||||
|
||||
Reference in New Issue
Block a user