Skip to content

Commit 02e7a96

Browse files
authored
feat/multipleruns (#163)
* feat: added the ability to run a workflow multiple times * fix: bug where notifications would appear globally regardless of which workflow you are on
1 parent 31012fc commit 02e7a96

File tree

5 files changed

+213
-65
lines changed

5 files changed

+213
-65
lines changed

sim/app/w/[id]/components/control-bar/control-bar.tsx

Lines changed: 154 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect, useState } from 'react'
44
import { useRouter } from 'next/navigation'
55
import { formatDistanceToNow } from 'date-fns'
6-
import { Bell, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react'
6+
import { Bell, ChevronDown, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react'
77
import {
88
AlertDialog,
99
AlertDialogAction,
@@ -22,6 +22,7 @@ import {
2222
DropdownMenuItem,
2323
DropdownMenuTrigger,
2424
} from '@/components/ui/dropdown-menu'
25+
import { Progress } from '@/components/ui/progress'
2526
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
2627
import { createLogger } from '@/lib/logs/console-logger'
2728
import { cn } from '@/lib/utils'
@@ -35,6 +36,9 @@ import { NotificationDropdownItem } from './components/notification-dropdown-ite
3536

3637
const logger = createLogger('ControlBar')
3738

39+
// Predefined run count options
40+
const RUN_COUNT_OPTIONS = [1, 5, 10, 25, 50, 100]
41+
3842
/**
3943
* Control bar for managing workflows - handles editing, deletion, deployment,
4044
* history, notifications and execution.
@@ -76,6 +80,12 @@ export function ControlBar() {
7680
// Marketplace modal state
7781
const [isMarketplaceModalOpen, setIsMarketplaceModalOpen] = useState(false)
7882

83+
// Multiple runs state
84+
const [runCount, setRunCount] = useState(1)
85+
const [completedRuns, setCompletedRuns] = useState(0)
86+
const [isMultiRunning, setIsMultiRunning] = useState(false)
87+
const [showRunProgress, setShowRunProgress] = useState(false)
88+
7989
// Get notifications for current workflow
8090
const workflowNotifications = activeWorkflowId
8191
? getWorkflowNotifications(activeWorkflowId)
@@ -286,6 +296,35 @@ export function ControlBar() {
286296
setIsMarketplaceModalOpen(true)
287297
}
288298

299+
/**
300+
* Handle multiple workflow runs
301+
*/
302+
const handleMultipleRuns = async () => {
303+
if (isExecuting || isMultiRunning || runCount <= 0) return
304+
305+
// Reset state for a new batch of runs
306+
setCompletedRuns(0)
307+
setIsMultiRunning(true)
308+
setShowRunProgress(runCount > 1)
309+
310+
try {
311+
// Run the workflow multiple times sequentially
312+
for (let i = 0; i < runCount; i++) {
313+
await handleRunWorkflow()
314+
setCompletedRuns(i + 1)
315+
}
316+
} catch (error) {
317+
logger.error('Error during multiple workflow runs:', { error })
318+
addNotification('error', 'Failed to complete all workflow runs', activeWorkflowId)
319+
} finally {
320+
setIsMultiRunning(false)
321+
// Keep progress visible for a moment after completion
322+
if (runCount > 1) {
323+
setTimeout(() => setShowRunProgress(false), 2000)
324+
}
325+
}
326+
}
327+
289328
/**
290329
* Render workflow name section (editable/non-editable)
291330
*/
@@ -449,44 +488,51 @@ export function ControlBar() {
449488
/**
450489
* Render notifications dropdown
451490
*/
452-
const renderNotificationsDropdown = () => (
453-
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
454-
<Tooltip>
455-
<TooltipTrigger asChild>
456-
<DropdownMenuTrigger asChild>
457-
<Button variant="ghost" size="icon">
458-
<Bell />
459-
<span className="sr-only">Notifications</span>
460-
</Button>
461-
</DropdownMenuTrigger>
462-
</TooltipTrigger>
463-
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
464-
</Tooltip>
465-
466-
{workflowNotifications.length === 0 ? (
467-
<DropdownMenuContent align="end" className="w-40">
468-
<DropdownMenuItem className="text-sm text-muted-foreground">
469-
No new notifications
470-
</DropdownMenuItem>
471-
</DropdownMenuContent>
472-
) : (
473-
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
474-
{[...workflowNotifications]
475-
.sort((a, b) => b.timestamp - a.timestamp)
476-
.map((notification) => (
477-
<NotificationDropdownItem
478-
key={notification.id}
479-
id={notification.id}
480-
type={notification.type}
481-
message={notification.message}
482-
timestamp={notification.timestamp}
483-
options={notification.options}
484-
/>
485-
))}
486-
</DropdownMenuContent>
487-
)}
488-
</DropdownMenu>
489-
)
491+
const renderNotificationsDropdown = () => {
492+
// Ensure we're only showing notifications for the current workflow
493+
const currentWorkflowNotifications = activeWorkflowId
494+
? notifications.filter((n) => n.workflowId === activeWorkflowId)
495+
: []
496+
497+
return (
498+
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
499+
<Tooltip>
500+
<TooltipTrigger asChild>
501+
<DropdownMenuTrigger asChild>
502+
<Button variant="ghost" size="icon">
503+
<Bell />
504+
<span className="sr-only">Notifications</span>
505+
</Button>
506+
</DropdownMenuTrigger>
507+
</TooltipTrigger>
508+
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
509+
</Tooltip>
510+
511+
{currentWorkflowNotifications.length === 0 ? (
512+
<DropdownMenuContent align="end" className="w-40">
513+
<DropdownMenuItem className="text-sm text-muted-foreground">
514+
No new notifications
515+
</DropdownMenuItem>
516+
</DropdownMenuContent>
517+
) : (
518+
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
519+
{[...currentWorkflowNotifications]
520+
.sort((a, b) => b.timestamp - a.timestamp)
521+
.map((notification) => (
522+
<NotificationDropdownItem
523+
key={notification.id}
524+
id={notification.id}
525+
type={notification.type}
526+
message={notification.message}
527+
timestamp={notification.timestamp}
528+
options={notification.options}
529+
/>
530+
))}
531+
</DropdownMenuContent>
532+
)}
533+
</DropdownMenu>
534+
)
535+
}
490536

491537
/**
492538
* Render publish button
@@ -520,31 +566,78 @@ export function ControlBar() {
520566
)
521567

522568
/**
523-
* Render run workflow button
569+
* Render run workflow button with multi-run dropdown
524570
*/
525571
const renderRunButton = () => (
526-
<Button
527-
className={cn(
528-
// Base styles
529-
'gap-2 ml-1 font-medium',
530-
// Brand color with hover states
531-
'bg-[#7F2FFF] hover:bg-[#7028E6]',
532-
// Hover effect with brand color
533-
'shadow-[0_0_0_0_#7F2FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
534-
// Text color and transitions
535-
'text-white transition-all duration-200',
536-
// Running state animation
537-
isExecuting &&
538-
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
539-
// Disabled state
540-
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none'
572+
<div className="flex items-center">
573+
{showRunProgress && (
574+
<div className="mr-3 w-28">
575+
<Progress value={(completedRuns / runCount) * 100} className="h-2 bg-muted" />
576+
<p className="text-xs text-muted-foreground mt-1 text-center">
577+
{completedRuns}/{runCount} runs
578+
</p>
579+
</div>
541580
)}
542-
onClick={handleRunWorkflow}
543-
disabled={isExecuting}
544-
>
545-
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
546-
{isExecuting ? 'Running' : 'Run'}
547-
</Button>
581+
582+
<div className="flex ml-1">
583+
{/* Main Run Button */}
584+
<Button
585+
className={cn(
586+
'gap-2 font-medium',
587+
'bg-[#7F2FFF] hover:bg-[#7028E6]',
588+
'shadow-[0_0_0_0_#7F2FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
589+
'text-white transition-all duration-200',
590+
(isExecuting || isMultiRunning) &&
591+
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
592+
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
593+
'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
594+
)}
595+
onClick={handleMultipleRuns}
596+
disabled={isExecuting || isMultiRunning}
597+
>
598+
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
599+
{isMultiRunning
600+
? `Running ${completedRuns}/${runCount}`
601+
: isExecuting
602+
? 'Running'
603+
: runCount === 1
604+
? 'Run'
605+
: `Run (${runCount})`}
606+
</Button>
607+
608+
{/* Dropdown Trigger */}
609+
<DropdownMenu>
610+
<DropdownMenuTrigger asChild>
611+
<Button
612+
className={cn(
613+
'px-2 font-medium',
614+
'bg-[#7F2FFF] hover:bg-[#7028E6]',
615+
'shadow-[0_0_0_0_#7F2FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
616+
'text-white transition-all duration-200',
617+
(isExecuting || isMultiRunning) &&
618+
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
619+
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
620+
'rounded-l-none h-10'
621+
)}
622+
disabled={isExecuting || isMultiRunning}
623+
>
624+
<ChevronDown className="h-4 w-4" />
625+
</Button>
626+
</DropdownMenuTrigger>
627+
<DropdownMenuContent align="end" className="w-20">
628+
{RUN_COUNT_OPTIONS.map((count) => (
629+
<DropdownMenuItem
630+
key={count}
631+
onClick={() => setRunCount(count)}
632+
className={cn('justify-center', runCount === count && 'bg-muted')}
633+
>
634+
{count}
635+
</DropdownMenuItem>
636+
))}
637+
</DropdownMenuContent>
638+
</DropdownMenu>
639+
</div>
640+
</div>
548641
)
549642

550643
return (

sim/app/w/[id]/components/notifications/notifications.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createLogger } from '@/lib/logs/console-logger'
1818
import { cn } from '@/lib/utils'
1919
import { useNotificationStore } from '@/stores/notifications/store'
2020
import { Notification } from '@/stores/notifications/types'
21+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
2122
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2223

2324
const logger = createLogger('Notifications')
@@ -92,19 +93,22 @@ function DeleteApiConfirmation({
9293
export function NotificationList() {
9394
// Store access
9495
const { notifications, hideNotification } = useNotificationStore()
96+
const { activeWorkflowId } = useWorkflowRegistry()
9597

9698
// Local state
9799
const [fadingNotifications, setFadingNotifications] = useState<Set<string>>(new Set())
98100

99-
// Filter to only show visible notifications
100-
const visibleNotifications = notifications.filter((n) => n.isVisible)
101+
// Filter to only show visible notifications for the current workflow
102+
const visibleNotifications = notifications.filter(
103+
(n) => n.isVisible && n.workflowId === activeWorkflowId
104+
)
101105

102106
// Handle auto-dismissal of non-persistent notifications
103107
useEffect(() => {
104108
// Setup timers for each notification
105109
const timers: ReturnType<typeof setTimeout>[] = []
106110

107-
notifications.forEach((notification) => {
111+
visibleNotifications.forEach((notification) => {
108112
// Skip if already hidden or marked as persistent
109113
if (!notification.isVisible || notification.options?.isPersistent) return
110114

@@ -128,7 +132,7 @@ export function NotificationList() {
128132

129133
// Cleanup timers on unmount or when notifications change
130134
return () => timers.forEach(clearTimeout)
131-
}, [notifications, hideNotification])
135+
}, [visibleNotifications, hideNotification])
132136

133137
// Early return if no notifications to show
134138
if (visibleNotifications.length === 0) return null

sim/components/ui/progress.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import * as React from 'react'
4+
import * as ProgressPrimitive from '@radix-ui/react-progress'
5+
import { cn } from '@/lib/utils'
6+
7+
const Progress = React.forwardRef<
8+
React.ElementRef<typeof ProgressPrimitive.Root>,
9+
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
10+
>(({ className, value, ...props }, ref) => (
11+
<ProgressPrimitive.Root
12+
ref={ref}
13+
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-muted', className)}
14+
{...props}
15+
>
16+
<ProgressPrimitive.Indicator
17+
className="h-full w-full flex-1 bg-primary transition-all"
18+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
19+
/>
20+
</ProgressPrimitive.Root>
21+
))
22+
Progress.displayName = ProgressPrimitive.Root.displayName
23+
24+
export { Progress }

sim/package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sim/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@radix-ui/react-dropdown-menu": "^2.1.4",
3939
"@radix-ui/react-label": "^2.1.2",
4040
"@radix-ui/react-popover": "^1.1.5",
41+
"@radix-ui/react-progress": "^1.1.2",
4142
"@radix-ui/react-scroll-area": "^1.2.2",
4243
"@radix-ui/react-select": "^2.1.4",
4344
"@radix-ui/react-separator": "^1.1.2",

0 commit comments

Comments
 (0)