@@ -30,7 +30,7 @@ import { Feedback } from "~/components/Feedback";
30
30
import { PageBody , PageContainer } from "~/components/layout/AppLayout" ;
31
31
import { BigNumber } from "~/components/metrics/BigNumber" ;
32
32
import { Badge } from "~/components/primitives/Badge" ;
33
- import { Button , LinkButton } from "~/components/primitives/Buttons" ;
33
+ import { Button , ButtonVariant , LinkButton } from "~/components/primitives/Buttons" ;
34
34
import { Callout } from "~/components/primitives/Callout" ;
35
35
import { Dialog , DialogContent , DialogHeader , DialogTrigger } from "~/components/primitives/Dialog" ;
36
36
import { FormButtons } from "~/components/primitives/FormButtons" ;
@@ -48,6 +48,7 @@ import {
48
48
TableRow ,
49
49
} from "~/components/primitives/Table" ;
50
50
import {
51
+ InfoIconTooltip ,
51
52
SimpleTooltip ,
52
53
Tooltip ,
53
54
TooltipContent ,
@@ -65,13 +66,14 @@ import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePrese
65
66
import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server" ;
66
67
import { requireUserId } from "~/services/session.server" ;
67
68
import { cn } from "~/utils/cn" ;
68
- import { docsPath , EnvironmentParamSchema , v3BillingPath } from "~/utils/pathBuilder" ;
69
+ import { docsPath , EnvironmentParamSchema , v3BillingPath , v3RunsPath } from "~/utils/pathBuilder" ;
69
70
import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server" ;
70
71
import { PauseQueueService } from "~/v3/services/pauseQueue.server" ;
71
72
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route" ;
72
73
import { Header3 } from "~/components/primitives/Headers" ;
73
74
import { Input } from "~/components/primitives/Input" ;
74
75
import { useThrottle } from "~/hooks/useThrottle" ;
76
+ import { RunsIcon } from "~/assets/icons/RunsIcon" ;
75
77
76
78
const SearchParamsSchema = z . object ( {
77
79
query : z . string ( ) . optional ( ) ,
@@ -238,6 +240,16 @@ export default function Page() {
238
240
}
239
241
} , [ streamedEvents ] ) ;
240
242
243
+ const limitStatus =
244
+ environment . running === environment . concurrencyLimit * environment . burstFactor
245
+ ? "limit"
246
+ : environment . running > environment . concurrencyLimit
247
+ ? "burst"
248
+ : "within" ;
249
+
250
+ const limitClassName =
251
+ limitStatus === "burst" ? "text-warning" : limitStatus === "limit" ? "text-error" : undefined ;
252
+
241
253
return (
242
254
< PageContainer >
243
255
< NavBar >
@@ -261,30 +273,63 @@ export default function Page() {
261
273
value = { environment . queued }
262
274
suffix = { env . paused && environment . queued > 0 ? "paused" : undefined }
263
275
animate
264
- accessory = { < EnvironmentPauseResumeButton env = { env } /> }
276
+ accessory = {
277
+ < div className = "flex items-start gap-1" >
278
+ < LinkButton
279
+ variant = "tertiary/small"
280
+ to = { v3RunsPath ( organization , project , env , {
281
+ statuses : [ "PENDING" ] ,
282
+ period : "30d" ,
283
+ } ) }
284
+ >
285
+ View runs
286
+ </ LinkButton >
287
+ < EnvironmentPauseResumeButton env = { env } />
288
+ </ div >
289
+ }
265
290
valueClassName = { env . paused ? "text-warning" : undefined }
266
291
compactThreshold = { 1000000 }
267
292
/>
268
293
< BigNumber
269
294
title = "Running"
270
295
value = { environment . running }
271
296
animate
272
- valueClassName = {
273
- environment . running === environment . concurrencyLimit ? "text-warning" : undefined
274
- }
297
+ valueClassName = { limitClassName }
275
298
suffix = {
276
- environment . running === environment . concurrencyLimit
277
- ? "At concurrency limit"
278
- : undefined
299
+ limitStatus === "burst" ? (
300
+ < span className = { cn ( limitClassName , "flex items-center gap-1" ) } >
301
+ Including { environment . running - environment . concurrencyLimit } burst runs{ " " }
302
+ < BurstFactorTooltip environment = { environment } />
303
+ </ span >
304
+ ) : limitStatus === "limit" ? (
305
+ "At concurrency limit"
306
+ ) : undefined
307
+ }
308
+ accessory = {
309
+ < LinkButton
310
+ variant = "tertiary/small"
311
+ to = { v3RunsPath ( organization , project , env , {
312
+ statuses : [ "DEQUEUED" , "EXECUTING" ] ,
313
+ period : "30d" ,
314
+ } ) }
315
+ >
316
+ View runs
317
+ </ LinkButton >
279
318
}
280
319
compactThreshold = { 1000000 }
281
320
/>
282
321
< BigNumber
283
322
title = "Concurrency limit"
284
323
value = { environment . concurrencyLimit }
285
324
animate
286
- valueClassName = {
287
- environment . running === environment . concurrencyLimit ? "text-warning" : undefined
325
+ valueClassName = { limitClassName }
326
+ suffix = {
327
+ environment . burstFactor > 1 ? (
328
+ < span className = { cn ( limitClassName , "flex items-center gap-1" ) } >
329
+ Burst limit { environment . burstFactor * environment . concurrencyLimit } { " " }
330
+ < BurstFactorTooltip environment = { environment } />
331
+ </ span >
332
+ ) : undefined
288
333
}
289
334
accessory = {
290
335
plan ? (
@@ -323,7 +368,14 @@ export default function Page() {
323
368
pagination . totalPages > 1 && "grid-rows-[auto_1fr_auto]"
324
369
) }
325
370
>
326
- < QueueFilters />
371
+ < div className = "flex items-center gap-2 border-t border-grid-dimmed px-1.5 py-1.5" >
372
+ < QueueFilters />
373
+ < PaginationControls
374
+ currentPage = { pagination . currentPage }
375
+ totalPages = { pagination . totalPages }
376
+ showPageNumbers = { false }
377
+ />
378
+ </ div >
327
379
< Table containerClassName = "border-t" >
328
380
< TableHeader >
329
381
< TableRow >
@@ -370,6 +422,9 @@ export default function Page() {
370
422
queues . map ( ( queue ) => {
371
423
const limit = queue . concurrencyLimit ?? environment . concurrencyLimit ;
372
424
const isAtLimit = queue . running === limit ;
425
+ const queueFilterableName = `${ queue . type === "task" ? "task/" : "" } ${
426
+ queue . name
427
+ } `;
373
428
return (
374
429
< TableRow key = { queue . name } >
375
430
< TableCell >
@@ -450,6 +505,66 @@ export default function Page() {
450
505
hiddenButtons = {
451
506
! queue . paused && < QueuePauseResumeButton queue = { queue } />
452
507
}
508
+ popoverContent = {
509
+ < >
510
+ { queue . paused ? (
511
+ < QueuePauseResumeButton
512
+ queue = { queue }
513
+ variant = "minimal/small"
514
+ fullWidth
515
+ showTooltip = { false }
516
+ />
517
+ ) : (
518
+ < QueuePauseResumeButton
519
+ queue = { queue }
520
+ variant = "minimal/small"
521
+ fullWidth
522
+ showTooltip = { false }
523
+ />
524
+ ) }
525
+ < LinkButton
526
+ variant = "minimal/small"
527
+ to = { v3RunsPath ( organization , project , env , {
528
+ queues : [ queueFilterableName ] ,
529
+ period : "30d" ,
530
+ } ) }
531
+ fullWidth
532
+ textAlignLeft
533
+ LeadingIcon = { RunsIcon }
534
+ leadingIconClassName = "text-indigo-500"
535
+ >
536
+ View all runs
537
+ </ LinkButton >
538
+ < LinkButton
539
+ variant = "minimal/small"
540
+ to = { v3RunsPath ( organization , project , env , {
541
+ queues : [ queueFilterableName ] ,
542
+ statuses : [ "PENDING" ] ,
543
+ period : "30d" ,
544
+ } ) }
545
+ fullWidth
546
+ textAlignLeft
547
+ LeadingIcon = { RectangleStackIcon }
548
+ leadingIconClassName = "text-queues"
549
+ >
550
+ View queued runs
551
+ </ LinkButton >
552
+ < LinkButton
553
+ variant = "minimal/small"
554
+ to = { v3RunsPath ( organization , project , env , {
555
+ queues : [ queueFilterableName ] ,
556
+ statuses : [ "DEQUEUED" , "EXECUTING" ] ,
557
+ period : "30d" ,
558
+ } ) }
559
+ fullWidth
560
+ textAlignLeft
561
+ LeadingIcon = { Spinner }
562
+ leadingIconClassName = "size-4 animate-none"
563
+ >
564
+ View running runs
565
+ </ LinkButton >
566
+ </ >
567
+ }
453
568
/>
454
569
</ TableRow >
455
570
) ;
@@ -603,40 +718,56 @@ function EnvironmentPauseResumeButton({
603
718
604
719
function QueuePauseResumeButton ( {
605
720
queue,
721
+ variant = "tertiary/small" ,
722
+ fullWidth = false ,
723
+ showTooltip = true ,
606
724
} : {
607
725
/** The "id" here is a friendlyId */
608
726
queue : { id : string ; name : string ; paused : boolean } ;
727
+ variant ?: ButtonVariant ;
728
+ fullWidth ?: boolean ;
729
+ showTooltip ?: boolean ;
609
730
} ) {
610
731
const navigation = useNavigation ( ) ;
611
732
const [ isOpen , setIsOpen ] = useState ( false ) ;
612
733
734
+ const button = (
735
+ < Button
736
+ type = "button"
737
+ variant = { variant }
738
+ LeadingIcon = { queue . paused ? PlayIcon : PauseIcon }
739
+ leadingIconClassName = { queue . paused ? "text-success" : "text-warning" }
740
+ fullWidth = { fullWidth }
741
+ textAlignLeft = { fullWidth }
742
+ >
743
+ { queue . paused ? "Resume..." : "Pause..." }
744
+ </ Button >
745
+ ) ;
746
+
747
+ const trigger = showTooltip ? (
748
+ < div >
749
+ < TooltipProvider disableHoverableContent = { true } >
750
+ < Tooltip >
751
+ < TooltipTrigger asChild >
752
+ < div >
753
+ < DialogTrigger asChild > { button } </ DialogTrigger >
754
+ </ div >
755
+ </ TooltipTrigger >
756
+ < TooltipContent side = "right" className = { "text-xs" } >
757
+ { queue . paused
758
+ ? `Resume processing runs in queue "${ queue . name } "`
759
+ : `Pause processing runs in queue "${ queue . name } "` }
760
+ </ TooltipContent >
761
+ </ Tooltip >
762
+ </ TooltipProvider >
763
+ </ div >
764
+ ) : (
765
+ < DialogTrigger asChild > { button } </ DialogTrigger >
766
+ ) ;
767
+
613
768
return (
614
769
< Dialog open = { isOpen } onOpenChange = { setIsOpen } >
615
- < div >
616
- < TooltipProvider disableHoverableContent = { true } >
617
- < Tooltip >
618
- < TooltipTrigger asChild >
619
- < div >
620
- < DialogTrigger asChild >
621
- < Button
622
- type = "button"
623
- variant = "tertiary/small"
624
- LeadingIcon = { queue . paused ? PlayIcon : PauseIcon }
625
- leadingIconClassName = { queue . paused ? "text-success" : "text-warning" }
626
- >
627
- { queue . paused ? "Resume..." : "Pause..." }
628
- </ Button >
629
- </ DialogTrigger >
630
- </ div >
631
- </ TooltipTrigger >
632
- < TooltipContent side = "right" className = { "text-xs" } >
633
- { queue . paused
634
- ? `Resume processing runs in queue "${ queue . name } "`
635
- : `Pause processing runs in queue "${ queue . name } "` }
636
- </ TooltipContent >
637
- </ Tooltip >
638
- </ TooltipProvider >
639
- </ div >
770
+ { trigger }
640
771
< DialogContent >
641
772
< DialogHeader > { queue . paused ? "Resume queue?" : "Pause queue?" } </ DialogHeader >
642
773
< div className = "flex flex-col gap-3 pt-3" >
@@ -743,7 +874,7 @@ export function QueueFilters() {
743
874
const search = searchParams . get ( "query" ) ?? "" ;
744
875
745
876
return (
746
- < div className = "flex w-full border-t border-grid-dimmed px-1.5 py-1.5 " >
877
+ < div className = "flex grow " >
747
878
< Input
748
879
name = "search"
749
880
placeholder = "Search queue name"
@@ -756,3 +887,20 @@ export function QueueFilters() {
756
887
</ div >
757
888
) ;
758
889
}
890
+
891
+ function BurstFactorTooltip ( {
892
+ environment,
893
+ } : {
894
+ environment : { burstFactor : number ; concurrencyLimit : number } ;
895
+ } ) {
896
+ return (
897
+ < InfoIconTooltip
898
+ content = { `Your single queue concurrency limit is capped at ${
899
+ environment . concurrencyLimit
900
+ } , but you can burst up to ${
901
+ environment . burstFactor * environment . concurrencyLimit
902
+ } when across multiple queues/tasks.`}
903
+ contentClassName = "max-w-xs"
904
+ />
905
+ ) ;
906
+ }
0 commit comments