mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(stores): added deploy store and added subblock value to history
This commit is contained in:
@@ -21,6 +21,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
// Generate a new API key
|
||||
const apiKey = `wf_${uuidv4().replace(/-/g, '')}`
|
||||
const deployedAt = new Date()
|
||||
|
||||
// Update the workflow with the API key and deployment status
|
||||
await db
|
||||
@@ -28,11 +29,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
.set({
|
||||
apiKey,
|
||||
isDeployed: true,
|
||||
deployedAt: new Date(),
|
||||
deployedAt,
|
||||
})
|
||||
.where(eq(workflow.id, id))
|
||||
|
||||
return createSuccessResponse({ apiKey })
|
||||
return createSuccessResponse({ apiKey, isDeployed: true, deployedAt })
|
||||
} catch (error: any) {
|
||||
console.error('Error deploying workflow:', error)
|
||||
return createErrorResponse(error.message || 'Failed to deploy workflow', 500)
|
||||
|
||||
@@ -32,49 +32,44 @@ import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
|
||||
import { HistoryDropdownItem } from './components/history-dropdown-item'
|
||||
import { NotificationDropdownItem } from './components/notification-dropdown-item'
|
||||
|
||||
/**
|
||||
* Control bar for managing workflows - handles editing, deletion, deployment,
|
||||
* history, notifications and execution.
|
||||
*/
|
||||
export function ControlBar() {
|
||||
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
|
||||
const { history, undo, redo, revertToHistoryState, lastSaved } = useWorkflowStore()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedName, setEditedName] = useState('')
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
|
||||
const [, forceUpdate] = useState({})
|
||||
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
|
||||
const router = useRouter()
|
||||
|
||||
// Use client-side only rendering for the timestamp
|
||||
// Store hooks
|
||||
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
|
||||
const { history, revertToHistoryState, lastSaved, isDeployed, setDeploymentStatus } =
|
||||
useWorkflowStore()
|
||||
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
|
||||
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
|
||||
|
||||
// Local state
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
const [, forceUpdate] = useState({})
|
||||
|
||||
// Workflow name editing state
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedName, setEditedName] = useState('')
|
||||
|
||||
// Dropdown states
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||
|
||||
// Deployment states
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
|
||||
// Get notifications for current workflow
|
||||
const workflowNotifications = activeWorkflowId
|
||||
? getWorkflowNotifications(activeWorkflowId)
|
||||
: notifications // Show all if no workflow is active
|
||||
|
||||
const handleDeleteWorkflow = () => {
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Remove the workflow from the registry
|
||||
const newWorkflows = { ...workflows }
|
||||
delete newWorkflows[activeWorkflowId]
|
||||
|
||||
// Get remaining workflow IDs
|
||||
const remainingIds = Object.keys(newWorkflows)
|
||||
|
||||
// Navigate before removing the workflow to avoid any state inconsistencies
|
||||
if (remainingIds.length > 0) {
|
||||
router.push(`/w/${remainingIds[0]}`)
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// Remove the workflow from the registry
|
||||
removeWorkflow(activeWorkflowId)
|
||||
}
|
||||
// Client-side only rendering for the timestamp
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Update the time display every minute
|
||||
useEffect(() => {
|
||||
@@ -82,6 +77,30 @@ export function ControlBar() {
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Check deployment status on mount or when activeWorkflowId changes
|
||||
useEffect(() => {
|
||||
async function checkStatus() {
|
||||
if (!activeWorkflowId) return
|
||||
try {
|
||||
const response = await fetch(`/api/workflow/${activeWorkflowId}/status`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Update the store with the deployment status from the API
|
||||
setDeploymentStatus(
|
||||
data.isDeployed,
|
||||
data.deployedAt ? new Date(data.deployedAt) : undefined
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check deployment status:', error)
|
||||
}
|
||||
}
|
||||
checkStatus()
|
||||
}, [activeWorkflowId, setDeploymentStatus])
|
||||
|
||||
/**
|
||||
* Workflow name handlers
|
||||
*/
|
||||
const handleNameClick = () => {
|
||||
if (activeWorkflowId) {
|
||||
setEditedName(workflows[activeWorkflowId].name)
|
||||
@@ -107,28 +126,29 @@ export function ControlBar() {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the deployment state and handlers
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isDeployed, setIsDeployed] = useState(false)
|
||||
/**
|
||||
* Workflow deletion handler
|
||||
*/
|
||||
const handleDeleteWorkflow = () => {
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Check deployment status on mount
|
||||
useEffect(() => {
|
||||
async function checkStatus() {
|
||||
if (!activeWorkflowId) return
|
||||
try {
|
||||
const response = await fetch(`/api/workflow/${activeWorkflowId}/status`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setIsDeployed(data.isDeployed)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check deployment status:', error)
|
||||
}
|
||||
// Get remaining workflow IDs
|
||||
const remainingIds = Object.keys(workflows).filter((id) => id !== activeWorkflowId)
|
||||
|
||||
// Navigate before removing the workflow to avoid any state inconsistencies
|
||||
if (remainingIds.length > 0) {
|
||||
router.push(`/w/${remainingIds[0]}`)
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
checkStatus()
|
||||
}, [activeWorkflowId])
|
||||
|
||||
// Deploy the workflow
|
||||
// Remove the workflow from the registry
|
||||
removeWorkflow(activeWorkflowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow deployment handler
|
||||
*/
|
||||
const handleDeploy = async () => {
|
||||
if (!activeWorkflowId) return
|
||||
try {
|
||||
@@ -142,9 +162,12 @@ export function ControlBar() {
|
||||
|
||||
if (!response.ok) throw new Error('Failed to deploy workflow')
|
||||
|
||||
const { apiKey } = await response.json()
|
||||
const { apiKey, isDeployed: newDeployStatus, deployedAt } = await response.json()
|
||||
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflow/${activeWorkflowId}/execute`
|
||||
|
||||
// Update the store with the deployment status
|
||||
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
|
||||
|
||||
addNotification('api', 'Workflow successfully deployed', activeWorkflowId, {
|
||||
isPersistent: true,
|
||||
sections: [
|
||||
@@ -162,8 +185,6 @@ export function ControlBar() {
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
setIsDeployed(true)
|
||||
} catch (error) {
|
||||
addNotification('error', 'Failed to deploy workflow. Please try again.', activeWorkflowId)
|
||||
} finally {
|
||||
@@ -171,201 +192,236 @@ export function ControlBar() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render workflow name section (editable/non-editable)
|
||||
*/
|
||||
const renderWorkflowName = () => (
|
||||
<div className="flex flex-col gap-[2px]">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
onBlur={handleNameSubmit}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
autoFocus
|
||||
className="font-semibold text-sm bg-transparent border-none outline-none p-0 w-[200px]"
|
||||
/>
|
||||
) : (
|
||||
<h2
|
||||
className="font-semibold text-sm hover:text-muted-foreground w-fit"
|
||||
onClick={handleNameClick}
|
||||
>
|
||||
{activeWorkflowId ? workflows[activeWorkflowId]?.name : 'Workflow'}
|
||||
</h2>
|
||||
)}
|
||||
{mounted && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Saved{' '}
|
||||
{formatDistanceToNow(lastSaved || Date.now(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* Render delete workflow button with confirmation dialog
|
||||
*/
|
||||
const renderDeleteButton = () => (
|
||||
<AlertDialog>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={Object.keys(workflows).length <= 1}
|
||||
className="hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
<span className="sr-only">Delete Workflow</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete Workflow</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Workflow</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this workflow? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteWorkflow} className="bg-red-600 hover:bg-red-700">
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
|
||||
/**
|
||||
* Render deploy button with tooltip
|
||||
*/
|
||||
const renderDeployButton = () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDeploy}
|
||||
disabled={isDeploying}
|
||||
className={cn(
|
||||
'hover:text-foreground',
|
||||
isDeployed && 'text-green-500 hover:text-green-500'
|
||||
)}
|
||||
>
|
||||
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
|
||||
<span className="sr-only">Deploy API</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
/**
|
||||
* Render history dropdown
|
||||
*/
|
||||
const renderHistoryDropdown = () => (
|
||||
<DropdownMenu open={historyOpen} onOpenChange={setHistoryOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<History />
|
||||
<span className="sr-only">Version History</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
{!historyOpen && <TooltipContent>History</TooltipContent>}
|
||||
</Tooltip>
|
||||
|
||||
{history.past.length === 0 && history.future.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem className="text-sm text-muted-foreground">
|
||||
No history available
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
) : (
|
||||
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
|
||||
<>
|
||||
{[...history.future].reverse().map((entry, index) => (
|
||||
<HistoryDropdownItem
|
||||
key={`future-${entry.timestamp}-${index}`}
|
||||
action={entry.action}
|
||||
timestamp={entry.timestamp}
|
||||
onClick={() =>
|
||||
revertToHistoryState(
|
||||
history.past.length + 1 + (history.future.length - 1 - index)
|
||||
)
|
||||
}
|
||||
isFuture={true}
|
||||
/>
|
||||
))}
|
||||
<HistoryDropdownItem
|
||||
key={`current-${history.present.timestamp}`}
|
||||
action={history.present.action}
|
||||
timestamp={history.present.timestamp}
|
||||
isCurrent={true}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{[...history.past].reverse().map((entry, index) => (
|
||||
<HistoryDropdownItem
|
||||
key={`past-${entry.timestamp}-${index}`}
|
||||
action={entry.action}
|
||||
timestamp={entry.timestamp}
|
||||
onClick={() => revertToHistoryState(history.past.length - 1 - index)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
/**
|
||||
* Render notifications dropdown
|
||||
*/
|
||||
const renderNotificationsDropdown = () => (
|
||||
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell />
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
|
||||
</Tooltip>
|
||||
|
||||
{workflowNotifications.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem className="text-sm text-muted-foreground">
|
||||
No new notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
) : (
|
||||
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
|
||||
{[...workflowNotifications]
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.map((notification) => (
|
||||
<NotificationDropdownItem
|
||||
key={notification.id}
|
||||
id={notification.id}
|
||||
type={notification.type}
|
||||
message={notification.message}
|
||||
timestamp={notification.timestamp}
|
||||
options={notification.options}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
/**
|
||||
* Render run workflow button
|
||||
*/
|
||||
const renderRunButton = () => (
|
||||
<Button
|
||||
className="gap-2 bg-[#7F2FFF] hover:bg-[#7F2FFF]/90 text-white"
|
||||
onClick={handleRunWorkflow}
|
||||
disabled={isExecuting}
|
||||
>
|
||||
<Play fill="white" stroke="white" className="!h-3.5 !w-3.5" />
|
||||
{isExecuting ? 'Running...' : 'Run'}
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex h-16 w-full items-center justify-between bg-background px-6 border-b transition-all duration-300">
|
||||
{/* Left Section - Workflow Info */}
|
||||
<div className="flex flex-col gap-[2px]">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
onBlur={handleNameSubmit}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
autoFocus
|
||||
className="font-semibold text-sm bg-transparent border-none outline-none p-0 w-[200px]"
|
||||
/>
|
||||
) : (
|
||||
<h2
|
||||
className="font-semibold text-sm hover:text-muted-foreground w-fit"
|
||||
onClick={handleNameClick}
|
||||
>
|
||||
{activeWorkflowId ? workflows[activeWorkflowId].name : 'Workflow'}
|
||||
</h2>
|
||||
)}
|
||||
{mounted && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Saved{' '}
|
||||
{formatDistanceToNow(lastSaved || Date.now(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{renderWorkflowName()}
|
||||
|
||||
{/* Middle Section - Reserved for future use */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right Section - Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertDialog>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={Object.keys(workflows).length <= 1}
|
||||
className="hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
<span className="sr-only">Delete Workflow</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete Workflow</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Workflow</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this workflow? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteWorkflow}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDeploy}
|
||||
disabled={isDeploying}
|
||||
className={cn(
|
||||
'hover:text-foreground',
|
||||
isDeployed && 'text-green-500 hover:text-green-500'
|
||||
)}
|
||||
>
|
||||
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
|
||||
<span className="sr-only">Deploy API</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenu open={historyOpen} onOpenChange={setHistoryOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<History />
|
||||
<span className="sr-only">Version History</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
{!historyOpen && <TooltipContent>History</TooltipContent>}
|
||||
</Tooltip>
|
||||
|
||||
{history.past.length === 0 && history.future.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem className="text-sm text-muted-foreground">
|
||||
No history available
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
) : (
|
||||
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
|
||||
<>
|
||||
{[...history.future].reverse().map((entry, index) => (
|
||||
<HistoryDropdownItem
|
||||
key={`future-${entry.timestamp}-${index}`}
|
||||
action={entry.action}
|
||||
timestamp={entry.timestamp}
|
||||
onClick={() =>
|
||||
revertToHistoryState(
|
||||
history.past.length + 1 + (history.future.length - 1 - index)
|
||||
)
|
||||
}
|
||||
isFuture={true}
|
||||
/>
|
||||
))}
|
||||
<HistoryDropdownItem
|
||||
key={`current-${history.present.timestamp}`}
|
||||
action={history.present.action}
|
||||
timestamp={history.present.timestamp}
|
||||
isCurrent={true}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{[...history.past].reverse().map((entry, index) => (
|
||||
<HistoryDropdownItem
|
||||
key={`past-${entry.timestamp}-${index}`}
|
||||
action={entry.action}
|
||||
timestamp={entry.timestamp}
|
||||
onClick={() => revertToHistoryState(history.past.length - 1 - index)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell />
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
|
||||
</Tooltip>
|
||||
|
||||
{workflowNotifications.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem className="text-sm text-muted-foreground">
|
||||
No new notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
) : (
|
||||
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
|
||||
{[...workflowNotifications]
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.map((notification) => (
|
||||
<NotificationDropdownItem
|
||||
key={notification.id}
|
||||
id={notification.id}
|
||||
type={notification.type}
|
||||
message={notification.message}
|
||||
timestamp={notification.timestamp}
|
||||
options={notification.options}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
className="gap-2 bg-[#7F2FFF] hover:bg-[#7F2FFF]/90 text-white"
|
||||
onClick={handleRunWorkflow}
|
||||
disabled={isExecuting}
|
||||
>
|
||||
<Play fill="white" stroke="white" className="!h-3.5 !w-3.5" />
|
||||
{isExecuting ? 'Running...' : 'Run'}
|
||||
</Button>
|
||||
{renderDeleteButton()}
|
||||
{renderDeployButton()}
|
||||
{renderHistoryDropdown()}
|
||||
{renderNotificationsDropdown()}
|
||||
{renderRunButton()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface HistoryEntry {
|
||||
state: WorkflowState
|
||||
timestamp: number
|
||||
action: string
|
||||
subblockValues: Record<string, Record<string, any>>
|
||||
}
|
||||
|
||||
export interface WorkflowHistory {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { StateCreator } from 'zustand'
|
||||
import { HistoryActions, HistoryEntry, WorkflowHistory } from './history-types'
|
||||
import { useWorkflowRegistry } from './registry/store'
|
||||
import { useSubBlockStore } from './subblock/store'
|
||||
import { WorkflowState, WorkflowStore } from './types'
|
||||
import { mergeSubblockState } from './utils'
|
||||
|
||||
// MAX for each individual workflow
|
||||
const MAX_HISTORY_LENGTH = 20
|
||||
@@ -22,9 +25,12 @@ export const withHistory = (
|
||||
blocks: initialState.blocks,
|
||||
edges: initialState.edges,
|
||||
loops: initialState.loops,
|
||||
isDeployed: initialState.isDeployed || false,
|
||||
deployedAt: initialState.deployedAt,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {}, // Add storage for subblock values
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -49,6 +55,11 @@ export const withHistory = (
|
||||
const previous = history.past[history.past.length - 1]
|
||||
const newPast = history.past.slice(0, history.past.length - 1)
|
||||
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Apply the state change
|
||||
set({
|
||||
...state,
|
||||
...previous.state,
|
||||
@@ -58,6 +69,23 @@ export const withHistory = (
|
||||
future: [history.present, ...history.future],
|
||||
},
|
||||
})
|
||||
|
||||
// Restore subblock values from the previous state's snapshot
|
||||
if (previous.subblockValues && activeWorkflowId) {
|
||||
// Update the subblock store with the saved values
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: previous.subblockValues,
|
||||
},
|
||||
})
|
||||
|
||||
// Also update localStorage for backup
|
||||
localStorage.setItem(
|
||||
`subblock-values-${activeWorkflowId}`,
|
||||
JSON.stringify(previous.subblockValues)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// Restore next state from history
|
||||
@@ -68,6 +96,11 @@ export const withHistory = (
|
||||
const next = history.future[0]
|
||||
const newFuture = history.future.slice(1)
|
||||
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Apply the state change
|
||||
set({
|
||||
...state,
|
||||
...next.state,
|
||||
@@ -77,6 +110,23 @@ export const withHistory = (
|
||||
future: newFuture,
|
||||
},
|
||||
})
|
||||
|
||||
// Restore subblock values from the next state's snapshot
|
||||
if (next.subblockValues && activeWorkflowId) {
|
||||
// Update the subblock store with the saved values
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: next.subblockValues,
|
||||
},
|
||||
})
|
||||
|
||||
// Also update localStorage for backup
|
||||
localStorage.setItem(
|
||||
`subblock-values-${activeWorkflowId}`,
|
||||
JSON.stringify(next.subblockValues)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// Reset workflow to empty state
|
||||
@@ -88,9 +138,10 @@ export const withHistory = (
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks: {}, edges: [], loops: {} },
|
||||
state: { blocks: {}, edges: [], loops: {}, isDeployed: false },
|
||||
timestamp: Date.now(),
|
||||
action: 'Clear workflow',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -107,6 +158,10 @@ export const withHistory = (
|
||||
|
||||
if (!targetState) return
|
||||
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
const newPast = allStates.slice(0, index)
|
||||
const newFuture = allStates.slice(index + 1)
|
||||
|
||||
@@ -119,21 +174,60 @@ export const withHistory = (
|
||||
future: newFuture,
|
||||
},
|
||||
})
|
||||
|
||||
// Restore subblock values from the target state's snapshot
|
||||
if (targetState.subblockValues && activeWorkflowId) {
|
||||
// Update the subblock store with the saved values
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: targetState.subblockValues,
|
||||
},
|
||||
})
|
||||
|
||||
// Also update localStorage for backup
|
||||
localStorage.setItem(
|
||||
`subblock-values-${activeWorkflowId}`,
|
||||
JSON.stringify(targetState.subblockValues)
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new history entry with current state snapshot
|
||||
export const createHistoryEntry = (state: WorkflowState, action: string): HistoryEntry => ({
|
||||
state: {
|
||||
export const createHistoryEntry = (state: WorkflowState, action: string): HistoryEntry => {
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
|
||||
// Create a deep copy of the state
|
||||
const stateCopy = {
|
||||
blocks: { ...state.blocks },
|
||||
edges: [...state.edges],
|
||||
loops: { ...state.loops },
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action,
|
||||
})
|
||||
isDeployed: state.isDeployed !== undefined ? state.isDeployed : false,
|
||||
deployedAt: state.deployedAt,
|
||||
}
|
||||
|
||||
// Capture the current subblock values for this workflow
|
||||
let subblockValues = {}
|
||||
|
||||
if (activeWorkflowId) {
|
||||
// Get the current subblock values from the store
|
||||
const currentValues = useSubBlockStore.getState().workflowValues[activeWorkflowId] || {}
|
||||
|
||||
// Create a deep copy to ensure we don't have reference issues
|
||||
subblockValues = JSON.parse(JSON.stringify(currentValues))
|
||||
}
|
||||
|
||||
return {
|
||||
state: stateCopy,
|
||||
timestamp: Date.now(),
|
||||
action,
|
||||
subblockValues,
|
||||
}
|
||||
}
|
||||
|
||||
// Add new entry to history and maintain history size limit
|
||||
export const pushHistory = (
|
||||
|
||||
@@ -34,6 +34,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
edges: currentState.edges,
|
||||
loops: currentState.loops,
|
||||
history: currentState.history,
|
||||
isDeployed: currentState.isDeployed,
|
||||
deployedAt: currentState.deployedAt,
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -41,7 +43,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
// Load workflow state
|
||||
const savedState = localStorage.getItem(`workflow-${id}`)
|
||||
if (savedState) {
|
||||
const { blocks, edges, history, loops } = JSON.parse(savedState)
|
||||
const parsedState = JSON.parse(savedState)
|
||||
const { blocks, edges, history, loops } = parsedState
|
||||
|
||||
// Initialize subblock store with workflow values
|
||||
useSubBlockStore.getState().initializeFromWorkflow(id, blocks)
|
||||
@@ -50,12 +53,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
blocks,
|
||||
edges,
|
||||
loops,
|
||||
isDeployed: parsedState.isDeployed !== undefined ? parsedState.isDeployed : false,
|
||||
deployedAt: parsedState.deployedAt ? new Date(parsedState.deployedAt) : undefined,
|
||||
history: history || {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks, edges, loops: {} },
|
||||
state: {
|
||||
blocks,
|
||||
edges,
|
||||
loops: {},
|
||||
isDeployed: parsedState.isDeployed !== undefined ? parsedState.isDeployed : false,
|
||||
deployedAt: parsedState.deployedAt,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -65,12 +77,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks: {}, edges: [], loops: {} },
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -197,6 +218,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
@@ -206,9 +229,12 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -263,17 +289,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
newActiveWorkflowId = remainingIds[0]
|
||||
const savedState = localStorage.getItem(`workflow-${newActiveWorkflowId}`)
|
||||
if (savedState) {
|
||||
const { blocks, edges, history, loops } = JSON.parse(savedState)
|
||||
const { blocks, edges, history, loops, isDeployed, deployedAt } =
|
||||
JSON.parse(savedState)
|
||||
useWorkflowStore.setState({
|
||||
blocks,
|
||||
edges,
|
||||
loops,
|
||||
isDeployed: isDeployed || false,
|
||||
deployedAt: deployedAt ? new Date(deployedAt) : undefined,
|
||||
history: history || {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks, edges, loops },
|
||||
state: { blocks, edges, loops, isDeployed: isDeployed || false, deployedAt },
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -283,12 +313,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks: {}, edges: [], loops: {} },
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -354,6 +393,8 @@ const initializeRegistry = () => {
|
||||
edges: currentState.edges,
|
||||
loops: currentState.loops,
|
||||
history: currentState.history,
|
||||
isDeployed: currentState.isDeployed,
|
||||
deployedAt: currentState.deployedAt,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { WorkflowMetadata } from './types'
|
||||
// Available workflow colors
|
||||
export const WORKFLOW_COLORS = ['#3972F6', '#F639DD', '#F6B539', '#8139F6', '#F64439']
|
||||
|
||||
// Generates a unique name for a new workflow
|
||||
export function generateUniqueName(existingWorkflows: Record<string, WorkflowMetadata>): string {
|
||||
// Extract numbers from existing workflow names using regex
|
||||
const numbers = Object.values(existingWorkflows)
|
||||
@@ -21,11 +22,7 @@ export function generateUniqueName(existingWorkflows: Record<string, WorkflowMet
|
||||
return `Workflow ${nextNumber}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the next color to use for a new workflow based on the last used color
|
||||
* @param existingWorkflows - Current workflows in the registry
|
||||
* @returns The next color from the predefined color palette
|
||||
*/
|
||||
// Determines the next color to use for a new workflow based on the last used color
|
||||
export function getNextWorkflowColor(existingWorkflows: Record<string, WorkflowMetadata>): string {
|
||||
const workflowArray = Object.values(existingWorkflows)
|
||||
|
||||
|
||||
@@ -15,12 +15,15 @@ const initialState = {
|
||||
edges: [],
|
||||
loops: {},
|
||||
lastSaved: undefined,
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks: {}, edges: [], loops: {} },
|
||||
state: { blocks: {}, edges: [], loops: {}, isDeployed: false },
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -70,6 +73,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
isDeployed: get().isDeployed,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -100,6 +105,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
blocks: { ...get().blocks },
|
||||
edges: [...get().edges].filter((edge) => edge.source !== id && edge.target !== id),
|
||||
loops: { ...get().loops },
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
// Clean up subblock values before removing the block
|
||||
@@ -192,6 +199,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
blocks: { ...get().blocks },
|
||||
edges: newEdges,
|
||||
loops: newLoops,
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -229,6 +238,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
blocks: { ...get().blocks },
|
||||
edges: newEdges,
|
||||
loops: newLoops,
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -241,12 +252,21 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: { blocks: {}, edges: [], loops: {} },
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
@@ -270,6 +290,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
},
|
||||
edges: [...get().edges],
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -319,6 +341,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
// Update the subblock store with the duplicated values
|
||||
@@ -352,6 +376,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
},
|
||||
edges: [...get().edges],
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -369,6 +395,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -387,6 +415,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
edges: [...state.edges],
|
||||
loops: { ...get().loops },
|
||||
isDeployed: state.isDeployed || false,
|
||||
deployedAt: state.deployedAt,
|
||||
}))
|
||||
get().updateLastSaved()
|
||||
},
|
||||
@@ -401,6 +431,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
},
|
||||
edges: [...state.edges],
|
||||
isDeployed: state.isDeployed || false,
|
||||
deployedAt: state.deployedAt,
|
||||
}))
|
||||
get().updateLastSaved()
|
||||
},
|
||||
@@ -416,6 +448,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
maxIterations: Math.max(1, Math.min(50, maxIterations)), // Clamp between 1-50
|
||||
},
|
||||
},
|
||||
isDeployed: get().isDeployed || false,
|
||||
deployedAt: get().deployedAt,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -429,6 +463,17 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
lastUpdate: Date.now(),
|
||||
}))
|
||||
},
|
||||
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => {
|
||||
const newState = {
|
||||
...get(),
|
||||
isDeployed,
|
||||
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
|
||||
}
|
||||
|
||||
set(newState)
|
||||
get().updateLastSaved()
|
||||
},
|
||||
})),
|
||||
{ name: 'workflow-store' }
|
||||
)
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface WorkflowState {
|
||||
lastSaved?: number
|
||||
loops: Record<string, Loop>
|
||||
lastUpdate?: number
|
||||
isDeployed: boolean
|
||||
deployedAt?: Date
|
||||
}
|
||||
|
||||
export interface WorkflowActions {
|
||||
@@ -55,6 +57,7 @@ export interface WorkflowActions {
|
||||
updateBlockHeight: (id: string, height: number) => void
|
||||
updateLoopMaxIterations: (loopId: string, maxIterations: number) => void
|
||||
triggerUpdate: () => void
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
|
||||
}
|
||||
|
||||
export type WorkflowStore = WorkflowState & WorkflowActions
|
||||
|
||||
Reference in New Issue
Block a user