Skip to content

Commit 42089a4

Browse files
committed
Pausing individual queues working
1 parent 34a178f commit 42089a4

File tree

6 files changed

+294
-34
lines changed

6 files changed

+294
-34
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const variants = {
77
small:
88
"grid place-items-center rounded-full px-[0.4rem] h-4 tracking-wider text-xxs bg-background-dimmed text-text-dimmed uppercase whitespace-nowrap",
99
"extra-small":
10-
"grid place-items-center border border-charcoal-650 rounded-sm px-1 h-4 tracking-wide text-xxs bg-background-bright text-blue-500 whitespace-nowrap",
10+
"grid place-items-center border border-charcoal-650 rounded-sm px-1 h-4 text-xxs bg-background-bright text-blue-500 whitespace-nowrap",
1111
outline:
1212
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider text-xxs border border-dimmed text-text-dimmed uppercase whitespace-nowrap",
1313
"outline-rounded":

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { engine } from "~/v3/runEngine.server";
44
import { BasePresenter } from "./basePresenter.server";
55
import { toQueueItem } from "./QueueRetrievePresenter.server";
66

7-
const DEFAULT_ITEMS_PER_PAGE = 25;
7+
const DEFAULT_ITEMS_PER_PAGE = 10;
88
const MAX_ITEMS_PER_PAGE = 100;
99
export class QueueListPresenter extends BasePresenter {
1010
private readonly perPage: number;
@@ -60,6 +60,7 @@ export class QueueListPresenter extends BasePresenter {
6060
name: true,
6161
concurrencyLimit: true,
6262
type: true,
63+
paused: true,
6364
},
6465
orderBy: {
6566
name: "asc",
@@ -88,6 +89,7 @@ export class QueueListPresenter extends BasePresenter {
8889
running: results[1][queue.name] ?? 0,
8990
queued: results[0][queue.name] ?? 0,
9091
concurrencyLimit: queue.concurrencyLimit ?? null,
92+
paused: queue.paused,
9193
})
9294
);
9395
}

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

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ import { type TaskQueueType } from "@trigger.dev/database";
55
import { assertExhaustive } from "@trigger.dev/core";
66
import { determineEngineVersion } from "~/v3/engineVersion.server";
77
import { type QueueItem, type RetrieveQueueParam } from "@trigger.dev/core/v3";
8+
import { PrismaClientOrTransaction } from "@trigger.dev/database";
9+
10+
/**
11+
* Shared queue lookup logic used by both QueueRetrievePresenter and PauseQueueService
12+
*/
13+
export async function getQueue(
14+
prismaClient: PrismaClientOrTransaction,
15+
environment: AuthenticatedEnvironment,
16+
queue: RetrieveQueueParam
17+
) {
18+
if (typeof queue === "string") {
19+
return prismaClient.taskQueue.findFirst({
20+
where: {
21+
friendlyId: queue,
22+
runtimeEnvironmentId: environment.id,
23+
},
24+
});
25+
}
26+
27+
const queueName =
28+
queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name;
29+
return prismaClient.taskQueue.findFirst({
30+
where: {
31+
name: queueName,
32+
runtimeEnvironmentId: environment.id,
33+
},
34+
});
35+
}
836

937
export class QueueRetrievePresenter extends BasePresenter {
1038
public async call({
@@ -24,7 +52,7 @@ export class QueueRetrievePresenter extends BasePresenter {
2452
};
2553
}
2654

27-
const queue = await this.getQueue(environment, queueInput);
55+
const queue = await getQueue(this._replica, environment, queueInput);
2856
if (!queue) {
2957
return {
3058
success: false as const,
@@ -47,29 +75,10 @@ export class QueueRetrievePresenter extends BasePresenter {
4775
running: results[1]?.[queue.name] ?? 0,
4876
queued: results[0]?.[queue.name] ?? 0,
4977
concurrencyLimit: queue.concurrencyLimit ?? null,
78+
paused: queue.paused,
5079
}),
5180
};
5281
}
53-
54-
private async getQueue(environment: AuthenticatedEnvironment, queue: RetrieveQueueParam) {
55-
if (typeof queue === "string") {
56-
return this._replica.taskQueue.findFirst({
57-
where: {
58-
friendlyId: queue,
59-
runtimeEnvironmentId: environment.id,
60-
},
61-
});
62-
}
63-
64-
const queueName =
65-
queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name;
66-
return this._replica.taskQueue.findFirst({
67-
where: {
68-
name: queueName,
69-
runtimeEnvironmentId: environment.id,
70-
},
71-
});
72-
}
7382
}
7483

7584
function queueTypeFromType(type: TaskQueueType) {
@@ -95,6 +104,7 @@ export function toQueueItem(data: {
95104
running: number;
96105
queued: number;
97106
concurrencyLimit: number | null;
107+
paused: boolean;
98108
}): QueueItem {
99109
return {
100110
id: data.friendlyId,
@@ -104,5 +114,6 @@ export function toQueueItem(data: {
104114
running: data.running,
105115
queued: data.queued,
106116
concurrencyLimit: data.concurrencyLimit,
117+
paused: data.paused,
107118
};
108119
}

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

Lines changed: 165 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Table,
2525
TableBody,
2626
TableCell,
27+
TableCellMenu,
2728
TableHeader,
2829
TableHeaderCell,
2930
TableRow,
@@ -55,6 +56,8 @@ import { type RuntimeEnvironmentType } from "@trigger.dev/database";
5556
import { environmentFullTitle } from "~/components/environments/EnvironmentLabel";
5657
import { Callout } from "~/components/primitives/Callout";
5758
import upgradeForQueuesPath from "~/assets/images/queues-dashboard.png";
59+
import { PauseQueueService } from "~/v3/services/pauseQueue.server";
60+
import { Badge } from "~/components/primitives/Badge";
5861

5962
const SearchParamsSchema = z.object({
6063
page: z.coerce.number().min(1).default(1),
@@ -161,8 +164,40 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
161164
request,
162165
"Environment resumed"
163166
);
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+
}
164199
default:
165-
redirectWithErrorMessage(
200+
return redirectWithErrorMessage(
166201
`/orgs/${organizationSlug}/projects/${projectParam}/env/${envParam}/queues`,
167202
request,
168203
"Something went wrong"
@@ -266,7 +301,7 @@ export default function Page() {
266301
<TableHeaderCell alignment="right">Queued</TableHeaderCell>
267302
<TableHeaderCell alignment="right">Running</TableHeaderCell>
268303
<TableHeaderCell alignment="right">Concurrency limit</TableHeaderCell>
269-
<TableHeaderCell alignment="right">
304+
<TableHeaderCell className="w-[1%] pl-24">
270305
<span className="sr-only">Pause/resume</span>
271306
</TableHeaderCell>
272307
</TableRow>
@@ -287,37 +322,78 @@ export default function Page() {
287322
resolve={Promise.all([queues, environment])}
288323
errorElement={<Paragraph variant="small">Error loading queues</Paragraph>}
289324
>
290-
{([q, environment]) => {
325+
{([q, env]) => {
291326
return q.length > 0 ? (
292327
q.map((queue) => (
293328
<TableRow key={queue.name}>
294329
<TableCell>
295330
<span className="flex items-center gap-2">
296331
{queue.type === "task" ? (
297332
<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+
}
299341
content={`This queue was automatically created from your "${queue.name}" task`}
300342
/>
301343
) : (
302344
<SimpleTooltip
303345
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+
/>
305352
}
306353
content={`This is a custom queue you added in your code.`}
307354
/>
308355
)}
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}
310364
</span>
311365
</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+
>
315382
{queue.concurrencyLimit ?? (
316383
<span className="text-text-dimmed">
317-
Max ({environment.concurrencyLimit})
384+
Max ({env.concurrencyLimit})
318385
</span>
319386
)}
320387
</TableCell>
388+
<TableCellMenu
389+
isSticky
390+
visibleButtons={
391+
queue.paused && <QueuePauseResumeButton queue={queue} />
392+
}
393+
hiddenButtons={
394+
!queue.paused && <QueuePauseResumeButton queue={queue} />
395+
}
396+
/>
321397
</TableRow>
322398
))
323399
) : (
@@ -401,7 +477,7 @@ function EnvironmentPauseResumeButton({
401477
LeadingIcon={env.paused ? PlayIcon : PauseIcon}
402478
leadingIconClassName={env.paused ? "text-success" : "text-amber-500"}
403479
>
404-
{env.paused ? "Resume" : "Pause environment"}
480+
{env.paused ? "Resume..." : "Pause environment..."}
405481
</Button>
406482
</DialogTrigger>
407483
</div>
@@ -437,6 +513,7 @@ function EnvironmentPauseResumeButton({
437513
disabled={isLoading}
438514
variant={env.paused ? "primary/medium" : "danger/medium"}
439515
LeadingIcon={isLoading ? <Spinner /> : env.paused ? PlayIcon : PauseIcon}
516+
shortcut={{ modifiers: ["mod"], key: "enter" }}
440517
>
441518
{env.paused ? "Resume environment" : "Pause environment"}
442519
</Button>
@@ -456,6 +533,83 @@ function EnvironmentPauseResumeButton({
456533
);
457534
}
458535

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+
459613
function EngineVersionUpgradeCallout() {
460614
return (
461615
<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

Comments
 (0)