import { AdjustmentsHorizontalIcon, ArrowUpCircleIcon, BookOpenIcon, PauseIcon, PlayIcon, RectangleStackIcon, } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useNavigation, type MetaFunction } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import type { QueueItem } from "@trigger.dev/core/v3/schemas"; import type { RuntimeEnvironmentType } from "@trigger.dev/database"; import { useEffect, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import upgradeForQueuesPath from "~/assets/images/queues-dashboard.png"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { QueuesHasNoTasks } from "~/components/BlankStatePanels"; import { environmentFullTitle } from "~/components/environments/EnvironmentLabel"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { BigNumber } from "~/components/metrics/BigNumber"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton, type ButtonVariant } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; import { FormButtons } from "~/components/primitives/FormButtons"; import { Header3 } from "~/components/primitives/Headers"; import { Input } from "~/components/primitives/Input"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { PopoverMenuItem } from "~/components/primitives/Popover"; import { SearchInput } from "~/components/primitives/SearchInput"; import { Spinner } from "~/components/primitives/Spinner"; import { Table, TableBody, TableCell, TableCellMenu, TableHeader, TableHeaderCell, TableRow, } from "~/components/primitives/Table"; import { InfoIconTooltip, SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "~/components/primitives/Tooltip"; import { QueueName } from "~/components/runs/v3/QueueName"; import { env } from "~/env.server"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { getUserById } from "~/models/user.server"; import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePresenter.server"; import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT } from "~/utils/environmentPauseSource"; import { concurrencyPath, docsPath, EnvironmentParamSchema, v3BillingPath, v3RunsPath, } from "~/utils/pathBuilder"; import { concurrencySystem } from "~/v3/services/concurrencySystemInstance.server"; import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server"; import { PauseQueueService } from "~/v3/services/pauseQueue.server"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; const SearchParamsSchema = z.object({ query: z.string().optional(), page: z.coerce.number().min(1).default(1), }); export const meta: MetaFunction = () => { return [ { title: `Queues | Trigger.dev`, }, ]; }; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const url = new URL(request.url); const { page, query } = SearchParamsSchema.parse(Object.fromEntries(url.searchParams)); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response(undefined, { status: 404, statusText: "Project not found", }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response(undefined, { status: 404, statusText: "Environment not found", }); } try { const queueListPresenter = new QueueListPresenter(); const queues = await queueListPresenter.call({ environment, query, page, }); const environmentQueuePresenter = new EnvironmentQueuePresenter(); const autoReloadPollIntervalMs = env.QUEUES_AUTORELOAD_POLL_INTERVAL_MS; return typedjson({ ...queues, environment: await environmentQueuePresenter.call(environment), autoReloadPollIntervalMs, }); } catch (error) { console.error(error); throw new Response(undefined, { status: 400, statusText: "Something went wrong, if this problem persists please contact support.", }); } }; export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); if (request.method.toLowerCase() !== "post") { return redirectWithErrorMessage( `/orgs/${params.organizationSlug}/projects/${params.projectParam}/env/${params.envParam}/queues`, request, "Wrong method" ); } const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response(undefined, { status: 404, statusText: "Project not found", }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response(undefined, { status: 404, statusText: "Environment not found", }); } const formData = await request.formData(); const action = formData.get("action"); const url = new URL(request.url); const redirectPath = `/orgs/${organizationSlug}/projects/${projectParam}/env/${envParam}/queues${url.search}`; if (environment.archivedAt) { return redirectWithErrorMessage(redirectPath, request, "This branch is archived"); } switch (action) { case "environment-pause": { const pauseService = new PauseEnvironmentService(); const result = await pauseService.call(environment, "paused"); if (!result.success) { return redirectWithErrorMessage(redirectPath, request, result.error); } return redirectWithSuccessMessage(redirectPath, request, "Environment paused"); } case "environment-resume": { const resumeService = new PauseEnvironmentService(); const result = await resumeService.call(environment, "resumed"); if (!result.success) { return redirectWithErrorMessage(redirectPath, request, result.error); } return redirectWithSuccessMessage(redirectPath, request, "Environment resumed"); } case "queue-pause": case "queue-resume": { const friendlyId = formData.get("friendlyId"); if (!friendlyId) { return redirectWithErrorMessage(redirectPath, request, "Queue ID is required"); } const queueService = new PauseQueueService(); const result = await queueService.call( environment, friendlyId.toString(), action === "queue-pause" ? "paused" : "resumed" ); if (!result.success) { return redirectWithErrorMessage( redirectPath, request, result.error ?? `Failed to ${action === "queue-pause" ? "pause" : "resume"} queue` ); } return redirectWithSuccessMessage( redirectPath, request, `Queue ${action === "queue-pause" ? "paused" : "resumed"}` ); } case "queue-override": { const friendlyId = formData.get("friendlyId"); const concurrencyLimit = formData.get("concurrencyLimit"); if (!friendlyId) { return redirectWithErrorMessage(redirectPath, request, "Queue ID is required"); } if (!concurrencyLimit) { return redirectWithErrorMessage(redirectPath, request, "Concurrency limit is required"); } const limitNumber = parseInt(concurrencyLimit.toString(), 10); if (isNaN(limitNumber) || limitNumber < 0) { return redirectWithErrorMessage( redirectPath, request, "Concurrency limit must be a valid number" ); } const user = await getUserById(userId); if (!user) { return redirectWithErrorMessage(redirectPath, request, "User not found"); } const result = await concurrencySystem.queues.overrideQueueConcurrencyLimit( environment, friendlyId.toString(), limitNumber, user ); if (!result.isOk()) { return redirectWithErrorMessage( redirectPath, request, "Failed to override queue concurrency limit" ); } return redirectWithSuccessMessage( redirectPath, request, "Queue concurrency limit overridden" ); } case "queue-remove-override": { const friendlyId = formData.get("friendlyId"); if (!friendlyId) { return redirectWithErrorMessage(redirectPath, request, "Queue ID is required"); } const result = await concurrencySystem.queues.resetConcurrencyLimit( environment, friendlyId.toString() ); if (!result.isOk()) { return redirectWithErrorMessage( redirectPath, request, "Failed to reset queue concurrency limit" ); } return redirectWithSuccessMessage(redirectPath, request, "Queue concurrency limit reset"); } default: return redirectWithErrorMessage(redirectPath, request, "Something went wrong"); } }; export default function Page() { const { environment, queues, success, pagination, code, totalQueues, hasFilters, autoReloadPollIntervalMs, } = useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const env = useEnvironment(); const plan = useCurrentPlan(); useAutoRevalidate({ interval: autoReloadPollIntervalMs, onFocus: true }); const limitStatus = environment.running === environment.concurrencyLimit * environment.burstFactor ? "limit" : environment.running > environment.concurrencyLimit ? "burst" : "within"; const limitClassName = limitStatus === "burst" ? "text-warning" : limitStatus === "limit" ? "text-error" : undefined; return ( Queues docs
paused : undefined} animate accessory={
{environment.runsEnabled && env.pauseSource !== ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT ? ( ) : null}
} valueClassName={env.paused ? "text-warning tabular-nums" : "tabular-nums"} compactThreshold={1000000} /> Including {environment.running - environment.concurrencyLimit} burst runs{" "} ) : limitStatus === "limit" ? ( "At concurrency limit" ) : undefined } accessory={ } compactThreshold={1000000} /> 1 ? ( Burst limit {environment.burstFactor * environment.concurrencyLimit}{" "} ) : undefined } accessory={ plan ? ( plan?.v3Subscription?.plan?.limits.concurrentRuns.canExceed ? ( Increase limit ) : ( Increase limit ) ) : null } />
{success ? (
Name Queued Running Limit
Environment This queue is limited by your environment's concurrency limit of{" "} {environment.concurrencyLimit}.
User This queue is limited by a concurrency limit set in your code.
Override This queue's concurrency limit has been manually overridden from the dashboard or API.
} > Limited by
Pause/resume
{queues.length > 0 ? ( queues.map((queue) => { const limit = queue.concurrencyLimit ?? environment.concurrencyLimit; const isAtConcurrencyLimit = queue.running >= limit; const isAtQueueLimit = environment.queueSizeLimit !== null && queue.queued >= environment.queueSizeLimit; const queueFilterableName = `${queue.type === "task" ? "task/" : ""}${ queue.name }`; return ( {queue.concurrency?.overriddenAt ? ( Concurrency limit overridden } content="This queue's concurrency limit has been manually overridden from the dashboard or API." className="max-w-xs" disableHoverableContent /> ) : null} {queue.paused ? ( Paused ) : null} {isAtQueueLimit ? ( At queue limit ) : null} {isAtConcurrencyLimit ? ( At concurrency limit ) : null} {queue.queued} 0 && "text-text-bright", isAtConcurrencyLimit && "text-warning" )} > {queue.running} {limit} {queue.concurrency?.overriddenAt ? ( Override ) : queue.concurrencyLimit ? ( "User" ) : ( "Environment" )} } hiddenButtons={ !queue.paused && } popoverContent={ <> {queue.paused ? ( ) : ( )} } /> ); }) ) : (
{hasFilters ? "No queues found matching your filters" : "No queues found"}
)}
) : (
{totalQueues === 0 ? (
) : code === "engine-version" ? ( ) : ( Something went wrong )}
)}
); } function EnvironmentPauseResumeButton({ env, }: { env: { type: RuntimeEnvironmentType; paused: boolean }; }) { const navigation = useNavigation(); const [isOpen, setIsOpen] = useState(false); useEffect(() => { if (navigation.state === "loading" || navigation.state === "idle") { setIsOpen(false); } }, [navigation.state]); const isLoading = Boolean( navigation.formData?.get("action") === (env.paused ? "environment-resume" : "environment-pause") ); return (
{env.paused ? `Resume processing runs in ${environmentFullTitle(env)}` : `Pause processing runs in ${environmentFullTitle(env)}`}
{env.paused ? "Resume environment?" : "Pause environment?"}
{env.paused ? `This will allow runs to be dequeued in ${environmentFullTitle(env)} again.` : `This will pause all runs from being dequeued in ${environmentFullTitle( env )}. Any executing runs will continue to run.`}
setIsOpen(false)}> : env.paused ? PlayIcon : PauseIcon } shortcut={{ modifiers: ["mod"], key: "enter" }} > {env.paused ? "Resume environment" : "Pause environment"} } cancelButton={ } />
); } function QueuePauseResumeButton({ queue, variant = "tertiary/small", fullWidth = false, showTooltip = true, }: { /** The "id" here is a friendlyId */ queue: { id: string; name: string; paused: boolean }; variant?: ButtonVariant; fullWidth?: boolean; showTooltip?: boolean; }) { const [isOpen, setIsOpen] = useState(false); const trigger = showTooltip ? (
{queue.paused ? `Resume processing runs in queue "${queue.name}"` : `Pause processing runs in queue "${queue.name}"`}
) : ( ); return ( {trigger} {queue.paused ? "Resume queue?" : "Pause queue?"}
{queue.paused ? `This will allow runs to be dequeued in the "${queue.name}" queue again.` : `This will pause all runs from being dequeued in the "${queue.name}" queue. Any executing runs will continue to run.`}
setIsOpen(false)}> {queue.paused ? "Resume queue" : "Pause queue"} } cancelButton={ } />
); } function QueueOverrideConcurrencyButton({ queue, environmentConcurrencyLimit, }: { queue: QueueItem; environmentConcurrencyLimit: number; }) { const navigation = useNavigation(); const [isOpen, setIsOpen] = useState(false); const [concurrencyLimit, setConcurrencyLimit] = useState( queue.concurrencyLimit?.toString() ?? environmentConcurrencyLimit.toString() ); const isOverridden = !!queue.concurrency?.overriddenAt; const currentLimit = queue.concurrencyLimit ?? environmentConcurrencyLimit; useEffect(() => { if (navigation.state === "loading" || navigation.state === "idle") { setIsOpen(false); } }, [navigation.state]); const isLoading = Boolean( navigation.formData?.get("action") === "queue-override" || navigation.formData?.get("action") === "queue-remove-override" ); return ( {isOverridden ? "Edit concurrency override" : "Override concurrency limit"}
{isOverridden ? ( This queue's concurrency limit is currently overridden to {currentLimit}. {typeof queue.concurrency?.base === "number" && ` The original limit set in code was ${queue.concurrency.base}.`}{" "} You can update the override or remove it to restore the{" "} {typeof queue.concurrency?.base === "number" ? "limit set in code" : "environment concurrency limit"} . ) : ( Override this queue's concurrency limit. The current limit is {currentLimit}, which is set {queue.concurrencyLimit !== null ? "in code" : "by the environment"}. )}
setIsOpen(false)} className="space-y-3">
setConcurrencyLimit(e.target.value)} placeholder={currentLimit.toString()} autoFocus />
} shortcut={{ modifiers: ["mod"], key: "enter" }} > {isOverridden ? "Update override" : "Override limit"} } cancelButton={
{isOverridden && ( )}
} />
); } function EngineVersionUpgradeCallout() { return (

New queues table

Upgrade guide
Upgrade to SDK version 4+ to view the new queues table, and be able to pause and resume individual queues. Upgrade for queues
); } export function isEnvironmentPauseResumeFormSubmission( formMethod: string | undefined, formData: FormData | undefined ) { if (!formMethod || !formData) { return false; } return ( formMethod.toLowerCase() === "post" && (formData.get("action") === "environment-pause" || formData.get("action") === "environment-resume") ); } export function QueueFilters() { return ; } function BurstFactorTooltip({ environment, }: { environment: { burstFactor: number; concurrencyLimit: number }; }) { return ( ); }