@@ -24,6 +24,7 @@ import {
24
24
Table ,
25
25
TableBody ,
26
26
TableCell ,
27
+ TableCellMenu ,
27
28
TableHeader ,
28
29
TableHeaderCell ,
29
30
TableRow ,
@@ -55,6 +56,8 @@ import { type RuntimeEnvironmentType } from "@trigger.dev/database";
55
56
import { environmentFullTitle } from "~/components/environments/EnvironmentLabel" ;
56
57
import { Callout } from "~/components/primitives/Callout" ;
57
58
import upgradeForQueuesPath from "~/assets/images/queues-dashboard.png" ;
59
+ import { PauseQueueService } from "~/v3/services/pauseQueue.server" ;
60
+ import { Badge } from "~/components/primitives/Badge" ;
58
61
59
62
const SearchParamsSchema = z . object ( {
60
63
page : z . coerce . number ( ) . min ( 1 ) . default ( 1 ) ,
@@ -161,8 +164,40 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
161
164
request ,
162
165
"Environment resumed"
163
166
) ;
167
+ case "queue-pause" :
168
+ case "queue-resume" : {
169
+ const friendlyId = formData . get ( "friendlyId" ) ;
170
+ if ( ! friendlyId ) {
171
+ return redirectWithErrorMessage (
172
+ `/orgs/${ organizationSlug } /projects/${ projectParam } /env/${ envParam } /queues` ,
173
+ request ,
174
+ "Queue ID is required"
175
+ ) ;
176
+ }
177
+
178
+ const queueService = new PauseQueueService ( ) ;
179
+ const result = await queueService . call (
180
+ environment ,
181
+ friendlyId . toString ( ) ,
182
+ action === "queue-pause" ? "paused" : "resumed"
183
+ ) ;
184
+
185
+ if ( ! result . success ) {
186
+ return redirectWithErrorMessage (
187
+ `/orgs/${ organizationSlug } /projects/${ projectParam } /env/${ envParam } /queues` ,
188
+ request ,
189
+ result . error ?? `Failed to ${ action === "queue-pause" ? "pause" : "resume" } queue`
190
+ ) ;
191
+ }
192
+
193
+ return redirectWithSuccessMessage (
194
+ `/orgs/${ organizationSlug } /projects/${ projectParam } /env/${ envParam } /queues` ,
195
+ request ,
196
+ `Queue ${ action === "queue-pause" ? "paused" : "resumed" } `
197
+ ) ;
198
+ }
164
199
default :
165
- redirectWithErrorMessage (
200
+ return redirectWithErrorMessage (
166
201
`/orgs/${ organizationSlug } /projects/${ projectParam } /env/${ envParam } /queues` ,
167
202
request ,
168
203
"Something went wrong"
@@ -266,7 +301,7 @@ export default function Page() {
266
301
< TableHeaderCell alignment = "right" > Queued</ TableHeaderCell >
267
302
< TableHeaderCell alignment = "right" > Running</ TableHeaderCell >
268
303
< TableHeaderCell alignment = "right" > Concurrency limit</ TableHeaderCell >
269
- < TableHeaderCell alignment = "right ">
304
+ < TableHeaderCell className = "w-[1%] pl-24 ">
270
305
< span className = "sr-only" > Pause/resume</ span >
271
306
</ TableHeaderCell >
272
307
</ TableRow >
@@ -287,37 +322,78 @@ export default function Page() {
287
322
resolve = { Promise . all ( [ queues , environment ] ) }
288
323
errorElement = { < Paragraph variant = "small" > Error loading queues</ Paragraph > }
289
324
>
290
- { ( [ q , environment ] ) => {
325
+ { ( [ q , env ] ) => {
291
326
return q . length > 0 ? (
292
327
q . map ( ( queue ) => (
293
328
< TableRow key = { queue . name } >
294
329
< TableCell >
295
330
< span className = "flex items-center gap-2" >
296
331
{ queue . type === "task" ? (
297
332
< SimpleTooltip
298
- button = { < TaskIcon className = "size-4 text-blue-500" /> }
333
+ button = {
334
+ < TaskIcon
335
+ className = { cn (
336
+ "size-4 text-blue-500" ,
337
+ queue . paused && "opacity-50"
338
+ ) }
339
+ />
340
+ }
299
341
content = { `This queue was automatically created from your "${ queue . name } " task` }
300
342
/>
301
343
) : (
302
344
< SimpleTooltip
303
345
button = {
304
- < RectangleStackIcon className = "size-4 text-purple-500" />
346
+ < RectangleStackIcon
347
+ className = { cn (
348
+ "size-4 text-purple-500" ,
349
+ queue . paused && "opacity-50"
350
+ ) }
351
+ />
305
352
}
306
353
content = { `This is a custom queue you added in your code.` }
307
354
/>
308
355
) }
309
- < span > { queue . name } </ span >
356
+ < span className = { queue . paused ? "opacity-50" : undefined } >
357
+ { queue . name }
358
+ </ span >
359
+ { queue . paused ? (
360
+ < Badge variant = "extra-small" className = "text-warning" >
361
+ Paused
362
+ </ Badge >
363
+ ) : null }
310
364
</ span >
311
365
</ TableCell >
312
- < TableCell alignment = "right" > { queue . queued } </ TableCell >
313
- < TableCell alignment = "right" > { queue . running } </ TableCell >
314
- < TableCell alignment = "right" >
366
+ < TableCell
367
+ alignment = "right"
368
+ className = { queue . paused ? "opacity-50" : undefined }
369
+ >
370
+ { queue . queued }
371
+ </ TableCell >
372
+ < TableCell
373
+ alignment = "right"
374
+ className = { queue . paused ? "opacity-50" : undefined }
375
+ >
376
+ { queue . running }
377
+ </ TableCell >
378
+ < TableCell
379
+ alignment = "right"
380
+ className = { queue . paused ? "opacity-50" : undefined }
381
+ >
315
382
{ queue . concurrencyLimit ?? (
316
383
< span className = "text-text-dimmed" >
317
- Max ({ environment . concurrencyLimit } )
384
+ Max ({ env . concurrencyLimit } )
318
385
</ span >
319
386
) }
320
387
</ TableCell >
388
+ < TableCellMenu
389
+ isSticky
390
+ visibleButtons = {
391
+ queue . paused && < QueuePauseResumeButton queue = { queue } />
392
+ }
393
+ hiddenButtons = {
394
+ ! queue . paused && < QueuePauseResumeButton queue = { queue } />
395
+ }
396
+ />
321
397
</ TableRow >
322
398
) )
323
399
) : (
@@ -401,7 +477,7 @@ function EnvironmentPauseResumeButton({
401
477
LeadingIcon = { env . paused ? PlayIcon : PauseIcon }
402
478
leadingIconClassName = { env . paused ? "text-success" : "text-amber-500" }
403
479
>
404
- { env . paused ? "Resume" : "Pause environment" }
480
+ { env . paused ? "Resume... " : "Pause environment... " }
405
481
</ Button >
406
482
</ DialogTrigger >
407
483
</ div >
@@ -437,6 +513,7 @@ function EnvironmentPauseResumeButton({
437
513
disabled = { isLoading }
438
514
variant = { env . paused ? "primary/medium" : "danger/medium" }
439
515
LeadingIcon = { isLoading ? < Spinner /> : env . paused ? PlayIcon : PauseIcon }
516
+ shortcut = { { modifiers : [ "mod" ] , key : "enter" } }
440
517
>
441
518
{ env . paused ? "Resume environment" : "Pause environment" }
442
519
</ Button >
@@ -456,6 +533,83 @@ function EnvironmentPauseResumeButton({
456
533
) ;
457
534
}
458
535
536
+ function QueuePauseResumeButton ( {
537
+ queue,
538
+ } : {
539
+ /** The "id" here is a friendlyId */
540
+ queue : { id : string ; name : string ; paused : boolean } ;
541
+ } ) {
542
+ const navigation = useNavigation ( ) ;
543
+ const [ isOpen , setIsOpen ] = useState ( false ) ;
544
+
545
+ return (
546
+ < Dialog open = { isOpen } onOpenChange = { setIsOpen } >
547
+ < div >
548
+ < TooltipProvider disableHoverableContent = { true } >
549
+ < Tooltip >
550
+ < TooltipTrigger asChild >
551
+ < div >
552
+ < DialogTrigger asChild >
553
+ < Button
554
+ type = "button"
555
+ variant = "tertiary/small"
556
+ LeadingIcon = { queue . paused ? PlayIcon : PauseIcon }
557
+ leadingIconClassName = { queue . paused ? "text-success" : "text-amber-500" }
558
+ >
559
+ { queue . paused ? "Resume..." : "Pause..." }
560
+ </ Button >
561
+ </ DialogTrigger >
562
+ </ div >
563
+ </ TooltipTrigger >
564
+ < TooltipContent side = "right" className = { "text-xs" } >
565
+ { queue . paused
566
+ ? `Resume processing runs in queue "${ queue . name } "`
567
+ : `Pause processing runs in queue "${ queue . name } "` }
568
+ </ TooltipContent >
569
+ </ Tooltip >
570
+ </ TooltipProvider >
571
+ </ div >
572
+ < DialogContent >
573
+ < DialogHeader > { queue . paused ? "Resume queue?" : "Pause queue?" } </ DialogHeader >
574
+ < div className = "flex flex-col gap-3 pt-3" >
575
+ < Paragraph >
576
+ { queue . paused
577
+ ? `This will allow runs to be dequeued in the "${ queue . name } " queue again.`
578
+ : `This will pause all runs from being dequeued in the "${ queue . name } " queue. Any executing runs will continue to run.` }
579
+ </ Paragraph >
580
+ < Form method = "post" onSubmit = { ( ) => setIsOpen ( false ) } >
581
+ < input
582
+ type = "hidden"
583
+ name = "action"
584
+ value = { queue . paused ? "queue-resume" : "queue-pause" }
585
+ />
586
+ < input type = "hidden" name = "friendlyId" value = { queue . id } />
587
+ < FormButtons
588
+ confirmButton = {
589
+ < Button
590
+ type = "submit"
591
+ shortcut = { { modifiers : [ "mod" ] , key : "enter" } }
592
+ variant = { queue . paused ? "primary/medium" : "danger/medium" }
593
+ LeadingIcon = { queue . paused ? PlayIcon : PauseIcon }
594
+ >
595
+ { queue . paused ? "Resume queue" : "Pause queue" }
596
+ </ Button >
597
+ }
598
+ cancelButton = {
599
+ < DialogClose asChild >
600
+ < Button type = "button" variant = "tertiary/medium" >
601
+ Cancel
602
+ </ Button >
603
+ </ DialogClose >
604
+ }
605
+ />
606
+ </ Form >
607
+ </ div >
608
+ </ DialogContent >
609
+ </ Dialog >
610
+ ) ;
611
+ }
612
+
459
613
function EngineVersionUpgradeCallout ( ) {
460
614
return (
461
615
< div className = "mt-4 flex max-w-lg flex-col gap-4 rounded-sm border border-grid-bright bg-background-bright px-4" >
0 commit comments