Skip to content

Commit 6791328

Browse files
authored
Queues dashboard update (burst concurrency) (#2293)
* Initial burst changes to queue page * Added tooltip and changed wording around * View runs from Queues page * Fix for ugly Version filter "Current" badge
1 parent 3dce1f6 commit 6791328

File tree

5 files changed

+196
-42
lines changed

5 files changed

+196
-42
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface BigNumberProps {
1414
valueClassName?: string;
1515
defaultValue?: number;
1616
accessory?: ReactNode;
17-
suffix?: string;
17+
suffix?: ReactNode;
1818
suffixClassName?: string;
1919
compactThreshold?: number;
2020
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ const allVariants = {
163163
variant: variant,
164164
};
165165

166+
export type ButtonVariant = keyof typeof variant;
167+
166168
export type ButtonContentPropsType = {
167169
children?: React.ReactNode;
168170
LeadingIcon?: RenderIcon;
@@ -173,7 +175,7 @@ export type ButtonContentPropsType = {
173175
textAlignLeft?: boolean;
174176
className?: string;
175177
shortcut?: ShortcutDefinition;
176-
variant: keyof typeof variant;
178+
variant: ButtonVariant;
177179
shortcutPosition?: "before-trailing-icon" | "after-trailing-icon";
178180
tooltip?: ReactNode;
179181
iconSpacing?: string;

apps/webapp/app/components/runs/v3/RunFilters.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,8 +1286,10 @@ function VersionsDropdown({
12861286
{filtered.length > 0
12871287
? filtered.map((version) => (
12881288
<SelectItem key={version.version} value={version.version}>
1289-
{version.version}{" "}
1290-
{version.isCurrent ? <Badge variant="extra-small">current</Badge> : null}
1289+
<span className="flex items-center gap-2">
1290+
<span className="grow truncate">{version.version}</span>
1291+
{version.isCurrent ? <Badge variant="extra-small">Current</Badge> : null}
1292+
</span>
12911293
</SelectItem>
12921294
))
12931295
: null}

apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type Environment = {
77
running: number;
88
queued: number;
99
concurrencyLimit: number;
10+
burstFactor: number;
1011
};
1112

1213
export class EnvironmentQueuePresenter extends BasePresenter {
@@ -26,6 +27,7 @@ export class EnvironmentQueuePresenter extends BasePresenter {
2627
running,
2728
queued,
2829
concurrencyLimit: environment.maximumConcurrencyLimit,
30+
burstFactor: environment.concurrencyLimitBurstFactor.toNumber(),
2931
};
3032
}
3133
}

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

Lines changed: 186 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Feedback } from "~/components/Feedback";
3030
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
3131
import { BigNumber } from "~/components/metrics/BigNumber";
3232
import { Badge } from "~/components/primitives/Badge";
33-
import { Button, LinkButton } from "~/components/primitives/Buttons";
33+
import { Button, ButtonVariant, LinkButton } from "~/components/primitives/Buttons";
3434
import { Callout } from "~/components/primitives/Callout";
3535
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
3636
import { FormButtons } from "~/components/primitives/FormButtons";
@@ -48,6 +48,7 @@ import {
4848
TableRow,
4949
} from "~/components/primitives/Table";
5050
import {
51+
InfoIconTooltip,
5152
SimpleTooltip,
5253
Tooltip,
5354
TooltipContent,
@@ -65,13 +66,14 @@ import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePrese
6566
import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server";
6667
import { requireUserId } from "~/services/session.server";
6768
import { cn } from "~/utils/cn";
68-
import { docsPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/pathBuilder";
69+
import { docsPath, EnvironmentParamSchema, v3BillingPath, v3RunsPath } from "~/utils/pathBuilder";
6970
import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server";
7071
import { PauseQueueService } from "~/v3/services/pauseQueue.server";
7172
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
7273
import { Header3 } from "~/components/primitives/Headers";
7374
import { Input } from "~/components/primitives/Input";
7475
import { useThrottle } from "~/hooks/useThrottle";
76+
import { RunsIcon } from "~/assets/icons/RunsIcon";
7577

7678
const 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

604719
function 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

Comments
 (0)