@@ -30,7 +30,7 @@ import { Feedback } from "~/components/Feedback";
3030import { PageBody , PageContainer } from "~/components/layout/AppLayout" ;
3131import { BigNumber } from "~/components/metrics/BigNumber" ;
3232import { Badge } from "~/components/primitives/Badge" ;
33- import { Button , LinkButton } from "~/components/primitives/Buttons" ;
33+ import { Button , ButtonVariant , LinkButton } from "~/components/primitives/Buttons" ;
3434import { Callout } from "~/components/primitives/Callout" ;
3535import { Dialog , DialogContent , DialogHeader , DialogTrigger } from "~/components/primitives/Dialog" ;
3636import { FormButtons } from "~/components/primitives/FormButtons" ;
@@ -48,6 +48,7 @@ import {
4848 TableRow ,
4949} from "~/components/primitives/Table" ;
5050import {
51+ InfoIconTooltip ,
5152 SimpleTooltip ,
5253 Tooltip ,
5354 TooltipContent ,
@@ -65,13 +66,14 @@ import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePrese
6566import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server" ;
6667import { requireUserId } from "~/services/session.server" ;
6768import { cn } from "~/utils/cn" ;
68- import { docsPath , EnvironmentParamSchema , v3BillingPath } from "~/utils/pathBuilder" ;
69+ import { docsPath , EnvironmentParamSchema , v3BillingPath , v3RunsPath } from "~/utils/pathBuilder" ;
6970import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server" ;
7071import { PauseQueueService } from "~/v3/services/pauseQueue.server" ;
7172import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route" ;
7273import { Header3 } from "~/components/primitives/Headers" ;
7374import { Input } from "~/components/primitives/Input" ;
7475import { useThrottle } from "~/hooks/useThrottle" ;
76+ import { RunsIcon } from "~/assets/icons/RunsIcon" ;
7577
7678const SearchParamsSchema = z . object ( {
7779 query : z . string ( ) . optional ( ) ,
@@ -238,6 +240,16 @@ export default function Page() {
238240 }
239241 } , [ streamedEvents ] ) ;
240242
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+
241253 return (
242254 < PageContainer >
243255 < NavBar >
@@ -261,30 +273,63 @@ export default function Page() {
261273 value = { environment . queued }
262274 suffix = { env . paused && environment . queued > 0 ? "paused" : undefined }
263275 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+ }
265290 valueClassName = { env . paused ? "text-warning" : undefined }
266291 compactThreshold = { 1000000 }
267292 />
268293 < BigNumber
269294 title = "Running"
270295 value = { environment . running }
271296 animate
272- valueClassName = {
273- environment . running === environment . concurrencyLimit ? "text-warning" : undefined
274- }
297+ valueClassName = { limitClassName }
275298 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 >
279318 }
280319 compactThreshold = { 1000000 }
281320 />
282321 < BigNumber
283322 title = "Concurrency limit"
284323 value = { environment . concurrencyLimit }
285324 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
288333 }
289334 accessory = {
290335 plan ? (
@@ -323,7 +368,14 @@ export default function Page() {
323368 pagination . totalPages > 1 && "grid-rows-[auto_1fr_auto]"
324369 ) }
325370 >
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 >
327379 < Table containerClassName = "border-t" >
328380 < TableHeader >
329381 < TableRow >
@@ -370,6 +422,9 @@ export default function Page() {
370422 queues . map ( ( queue ) => {
371423 const limit = queue . concurrencyLimit ?? environment . concurrencyLimit ;
372424 const isAtLimit = queue . running === limit ;
425+ const queueFilterableName = `${ queue . type === "task" ? "task/" : "" } ${
426+ queue . name
427+ } `;
373428 return (
374429 < TableRow key = { queue . name } >
375430 < TableCell >
@@ -450,6 +505,66 @@ export default function Page() {
450505 hiddenButtons = {
451506 ! queue . paused && < QueuePauseResumeButton queue = { queue } />
452507 }
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+ }
453568 />
454569 </ TableRow >
455570 ) ;
@@ -603,40 +718,56 @@ function EnvironmentPauseResumeButton({
603718
604719function QueuePauseResumeButton ( {
605720 queue,
721+ variant = "tertiary/small" ,
722+ fullWidth = false ,
723+ showTooltip = true ,
606724} : {
607725 /** The "id" here is a friendlyId */
608726 queue : { id : string ; name : string ; paused : boolean } ;
727+ variant ?: ButtonVariant ;
728+ fullWidth ?: boolean ;
729+ showTooltip ?: boolean ;
609730} ) {
610731 const navigation = useNavigation ( ) ;
611732 const [ isOpen , setIsOpen ] = useState ( false ) ;
612733
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+
613768 return (
614769 < 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 }
640771 < DialogContent >
641772 < DialogHeader > { queue . paused ? "Resume queue?" : "Pause queue?" } </ DialogHeader >
642773 < div className = "flex flex-col gap-3 pt-3" >
@@ -743,7 +874,7 @@ export function QueueFilters() {
743874 const search = searchParams . get ( "query" ) ?? "" ;
744875
745876 return (
746- < div className = "flex w-full border-t border-grid-dimmed px-1.5 py-1.5 " >
877+ < div className = "flex grow " >
747878 < Input
748879 name = "search"
749880 placeholder = "Search queue name"
@@ -756,3 +887,20 @@ export function QueueFilters() {
756887 </ div >
757888 ) ;
758889}
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