Skip to content

Commit 0fe85ea

Browse files
authored
Highlight queues when concurrency limit reached and new table col (#2008)
* Combines the Running and Concurrency limit cols into 1 * Display a badge when a queue is at the concurrency limit * Colors the Running/Limit column text amber if the concurrency limit is hit * Turns the “Running” big number amber and shows “At concurrency limit” text * BigNumber now handles big values using formatNumber and formatNumberCompact Also includes some responsive improvements to make sure things wrap when it gets tight * Adds a new col for showing how the queue is limited * Reinstates a threshold for making very big numbers compact * Added border to make the search bar not float
1 parent d2dde0a commit 0fe85ea

File tree

3 files changed

+178
-90
lines changed

3 files changed

+178
-90
lines changed

apps/webapp/app/components/metrics/BigNumber.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { type ReactNode } from "react";
2-
import { AnimatedNumber } from "../primitives/AnimatedNumber";
3-
import { Spinner } from "../primitives/Spinner";
42
import { cn } from "~/utils/cn";
3+
import { formatNumber, formatNumberCompact } from "~/utils/numberFormatter";
4+
import { Header3 } from "../primitives/Headers";
5+
import { Spinner } from "../primitives/Spinner";
6+
import { SimpleTooltip } from "../primitives/Tooltip";
7+
import { AnimatedNumber } from "../primitives/AnimatedNumber";
58

69
interface BigNumberProps {
710
title: ReactNode;
@@ -13,6 +16,7 @@ interface BigNumberProps {
1316
accessory?: ReactNode;
1417
suffix?: string;
1518
suffixClassName?: string;
19+
compactThreshold?: number;
1620
}
1721

1822
export function BigNumber({
@@ -25,25 +29,39 @@ export function BigNumber({
2529
accessory,
2630
animate = false,
2731
loading = false,
32+
compactThreshold,
2833
}: BigNumberProps) {
2934
const v = value ?? defaultValue;
35+
36+
const shouldCompact =
37+
typeof compactThreshold === "number" && v !== undefined && v >= compactThreshold;
38+
3039
return (
31-
<div className="grid grid-rows-[1.5rem_auto] gap-4 rounded-sm border border-grid-dimmed bg-background-bright p-4">
32-
<div className="flex items-center justify-between">
33-
<div className="text-2sm text-text-dimmed">{title}</div>
40+
<div className="flex flex-col justify-between gap-4 rounded-sm border border-grid-dimmed bg-background-bright p-4">
41+
<div className="flex flex-wrap items-center justify-between gap-2">
42+
<Header3 className="leading-6">{title}</Header3>
3443
{accessory && <div className="flex-shrink-0">{accessory}</div>}
3544
</div>
3645
<div
3746
className={cn(
38-
"h-[3.75rem] text-[3.75rem] font-normal tabular-nums leading-none text-text-bright",
47+
"text-[3.75rem] font-normal tabular-nums leading-none text-text-bright",
3948
valueClassName
4049
)}
4150
>
4251
{loading ? (
4352
<Spinner className="size-6" />
4453
) : v !== undefined ? (
45-
<div className="flex items-baseline gap-1">
46-
{animate ? <AnimatedNumber value={v} /> : v}
54+
<div className="flex flex-wrap items-baseline gap-2">
55+
{shouldCompact ? (
56+
<SimpleTooltip
57+
button={animate ? <AnimatedNumber value={v} /> : formatNumberCompact(v)}
58+
content={formatNumber(v)}
59+
/>
60+
) : animate ? (
61+
<AnimatedNumber value={v} />
62+
) : (
63+
formatNumber(v)
64+
)}
4765
{suffix && <div className={cn("text-xs", suffixClassName)}>{suffix}</div>}
4866
</div>
4967
) : (

apps/webapp/app/components/primitives/AnimatedNumber.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motion, useSpring, useTransform, useMotionValue, animate } from "framer-motion";
1+
import { animate, motion, useMotionValue, useTransform } from "framer-motion";
22
import { useEffect } from "react";
33

44
export function AnimatedNumber({ value }: { value: number }) {

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx

Lines changed: 151 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { docsPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/pathBui
6969
import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server";
7070
import { PauseQueueService } from "~/v3/services/pauseQueue.server";
7171
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
72+
import { Header3 } from "~/components/primitives/Headers";
7273
import { Input } from "~/components/primitives/Input";
7374
import { useThrottle } from "~/hooks/useThrottle";
7475

@@ -257,13 +258,30 @@ export default function Page() {
257258
suffix={env.paused && environment.queued > 0 ? "paused" : undefined}
258259
animate
259260
accessory={<EnvironmentPauseResumeButton env={env} />}
260-
valueClassName={env.paused ? "text-amber-500" : undefined}
261+
valueClassName={env.paused ? "text-warning" : undefined}
262+
compactThreshold={1000000}
263+
/>
264+
<BigNumber
265+
title="Running"
266+
value={environment.running}
267+
animate
268+
valueClassName={
269+
environment.running === environment.concurrencyLimit ? "text-warning" : undefined
270+
}
271+
suffix={
272+
environment.running === environment.concurrencyLimit
273+
? "At concurrency limit"
274+
: undefined
275+
}
276+
compactThreshold={1000000}
261277
/>
262-
<BigNumber title="Running" value={environment.running} animate />
263278
<BigNumber
264279
title="Concurrency limit"
265280
value={environment.concurrencyLimit}
266281
animate
282+
valueClassName={
283+
environment.running === environment.concurrencyLimit ? "text-warning" : undefined
284+
}
267285
accessory={
268286
plan ? (
269287
plan?.v3Subscription?.plan?.limits.concurrentRuns.canExceed ? (
@@ -307,7 +325,37 @@ export default function Page() {
307325
<TableRow>
308326
<TableHeaderCell>Name</TableHeaderCell>
309327
<TableHeaderCell alignment="right">Queued</TableHeaderCell>
310-
<TableHeaderCell alignment="right">Running</TableHeaderCell>
328+
<TableHeaderCell alignment="right">Running/limit</TableHeaderCell>
329+
<TableHeaderCell
330+
alignment="right"
331+
tooltip={
332+
<div className="max-w-xs space-y-2 p-1 text-left">
333+
<div className="space-y-0.5">
334+
<Header3>Environment</Header3>
335+
<Paragraph
336+
variant="small"
337+
className="!text-wrap text-text-dimmed"
338+
spacing
339+
>
340+
This queue is limited by your environment's concurrency limit of{" "}
341+
{environment.concurrencyLimit}.
342+
</Paragraph>
343+
</div>
344+
<div className="space-y-0.5">
345+
<Header3>User</Header3>
346+
<Paragraph
347+
variant="small"
348+
className="!text-wrap text-text-dimmed"
349+
spacing
350+
>
351+
This queue is limited by a concurrency limit set in your code.
352+
</Paragraph>
353+
</div>
354+
</div>
355+
}
356+
>
357+
Limited by
358+
</TableHeaderCell>
311359
<TableHeaderCell
312360
alignment="right"
313361
tooltip={
@@ -334,88 +382,110 @@ export default function Page() {
334382
>
335383
Release on waitpoint
336384
</TableHeaderCell>
337-
<TableHeaderCell alignment="right">Concurrency limit</TableHeaderCell>
338385
<TableHeaderCell className="w-[1%] pl-24">
339386
<span className="sr-only">Pause/resume</span>
340387
</TableHeaderCell>
341388
</TableRow>
342389
</TableHeader>
343390
<TableBody>
344391
{queues.length > 0 ? (
345-
queues.map((queue) => (
346-
<TableRow key={queue.name}>
347-
<TableCell>
348-
<span className="flex items-center gap-2">
349-
{queue.type === "task" ? (
350-
<SimpleTooltip
351-
button={
352-
<TaskIconSmall
353-
className={cn(
354-
"size-[1.125rem] text-blue-500",
355-
queue.paused && "opacity-50"
356-
)}
357-
/>
358-
}
359-
content={`This queue was automatically created from your "${queue.name}" task`}
360-
/>
361-
) : (
362-
<SimpleTooltip
363-
button={
364-
<RectangleStackIcon
365-
className={cn(
366-
"size-[1.125rem] text-purple-500",
367-
queue.paused && "opacity-50"
368-
)}
369-
/>
370-
}
371-
content={`This is a custom queue you added in your code.`}
372-
/>
373-
)}
374-
<span className={queue.paused ? "opacity-50" : undefined}>
375-
{queue.name}
392+
queues.map((queue) => {
393+
const limit = queue.concurrencyLimit ?? environment.concurrencyLimit;
394+
const isAtLimit = queue.running === limit;
395+
return (
396+
<TableRow key={queue.name}>
397+
<TableCell>
398+
<span className="flex items-center gap-2">
399+
{queue.type === "task" ? (
400+
<SimpleTooltip
401+
button={
402+
<TaskIconSmall
403+
className={cn(
404+
"size-[1.125rem] text-blue-500",
405+
queue.paused && "opacity-50"
406+
)}
407+
/>
408+
}
409+
content={`This queue was automatically created from your "${queue.name}" task`}
410+
/>
411+
) : (
412+
<SimpleTooltip
413+
button={
414+
<RectangleStackIcon
415+
className={cn(
416+
"size-[1.125rem] text-purple-500",
417+
queue.paused && "opacity-50"
418+
)}
419+
/>
420+
}
421+
content={`This is a custom queue you added in your code.`}
422+
/>
423+
)}
424+
<span className={queue.paused ? "opacity-50" : undefined}>
425+
{queue.name}
426+
</span>
427+
{queue.paused ? (
428+
<Badge variant="extra-small" className="text-warning">
429+
Paused
430+
</Badge>
431+
) : null}
432+
{isAtLimit ? (
433+
<Badge variant="extra-small" className="text-warning">
434+
At concurrency limit
435+
</Badge>
436+
) : null}
376437
</span>
377-
{queue.paused ? (
378-
<Badge variant="extra-small" className="text-warning">
379-
Paused
380-
</Badge>
381-
) : null}
382-
</span>
383-
</TableCell>
384-
<TableCell
385-
alignment="right"
386-
className={queue.paused ? "opacity-50" : undefined}
387-
>
388-
{queue.queued}
389-
</TableCell>
390-
<TableCell
391-
alignment="right"
392-
className={queue.paused ? "opacity-50" : undefined}
393-
>
394-
{queue.running}
395-
</TableCell>
396-
<TableCell
397-
alignment="right"
398-
className={queue.paused ? "opacity-50" : undefined}
399-
>
400-
{queue.releaseConcurrencyOnWaitpoint ? "Yes" : "No"}
401-
</TableCell>
402-
<TableCell
403-
alignment="right"
404-
className={queue.paused ? "opacity-50" : undefined}
405-
>
406-
{queue.concurrencyLimit ?? (
407-
<span className="text-text-dimmed">
408-
Max ({environment.concurrencyLimit})
438+
</TableCell>
439+
<TableCell
440+
alignment="right"
441+
className={queue.paused ? "opacity-50" : undefined}
442+
>
443+
{queue.queued}
444+
</TableCell>
445+
<TableCell
446+
alignment="right"
447+
className={cn(
448+
queue.paused ? "tabular-nums opacity-50" : undefined,
449+
isAtLimit && "text-warning"
450+
)}
451+
>
452+
{queue.running}/
453+
<span
454+
className={cn(
455+
"tabular-nums text-text-dimmed",
456+
isAtLimit && "text-warning"
457+
)}
458+
>
459+
{limit}
409460
</span>
410-
)}
411-
</TableCell>
412-
<TableCellMenu
413-
isSticky
414-
visibleButtons={queue.paused && <QueuePauseResumeButton queue={queue} />}
415-
hiddenButtons={!queue.paused && <QueuePauseResumeButton queue={queue} />}
416-
/>
417-
</TableRow>
418-
))
461+
</TableCell>
462+
<TableCell
463+
alignment="right"
464+
className={cn(
465+
queue.paused ? "opacity-50" : undefined,
466+
isAtLimit && "text-warning"
467+
)}
468+
>
469+
{queue.concurrencyLimit ? "User" : "Environment"}
470+
</TableCell>
471+
<TableCell
472+
alignment="right"
473+
className={queue.paused ? "opacity-50" : undefined}
474+
>
475+
{queue.releaseConcurrencyOnWaitpoint ? "Yes" : "No"}
476+
</TableCell>
477+
<TableCellMenu
478+
isSticky
479+
visibleButtons={
480+
queue.paused && <QueuePauseResumeButton queue={queue} />
481+
}
482+
hiddenButtons={
483+
!queue.paused && <QueuePauseResumeButton queue={queue} />
484+
}
485+
/>
486+
</TableRow>
487+
);
488+
})
419489
) : (
420490
<TableRow>
421491
<TableCell colSpan={6}>
@@ -503,7 +573,7 @@ function EnvironmentPauseResumeButton({
503573
type="button"
504574
variant="secondary/small"
505575
LeadingIcon={env.paused ? PlayIcon : PauseIcon}
506-
leadingIconClassName={env.paused ? "text-success" : "text-amber-500"}
576+
leadingIconClassName={env.paused ? "text-success" : "text-warning"}
507577
>
508578
{env.paused ? "Resume..." : "Pause environment..."}
509579
</Button>
@@ -512,8 +582,8 @@ function EnvironmentPauseResumeButton({
512582
</TooltipTrigger>
513583
<TooltipContent className={"text-xs"}>
514584
{env.paused
515-
? `Resume processing runs in ${environmentFullTitle(env)}.`
516-
: `Pause processing runs in ${environmentFullTitle(env)}.`}
585+
? `Resume processing runs in ${environmentFullTitle(env)}`
586+
: `Pause processing runs in ${environmentFullTitle(env)}`}
517587
</TooltipContent>
518588
</Tooltip>
519589
</TooltipProvider>
@@ -582,7 +652,7 @@ function QueuePauseResumeButton({
582652
type="button"
583653
variant="tertiary/small"
584654
LeadingIcon={queue.paused ? PlayIcon : PauseIcon}
585-
leadingIconClassName={queue.paused ? "text-success" : "text-amber-500"}
655+
leadingIconClassName={queue.paused ? "text-success" : "text-warning"}
586656
>
587657
{queue.paused ? "Resume..." : "Pause..."}
588658
</Button>
@@ -703,7 +773,7 @@ export function QueueFilters() {
703773
const search = searchParams.get("query") ?? "";
704774

705775
return (
706-
<div className="flex w-full px-3 pb-3">
776+
<div className="flex w-full border-t border-grid-dimmed px-1.5 py-1.5">
707777
<Input
708778
name="search"
709779
placeholder="Search queue name"

0 commit comments

Comments
 (0)