Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{buttonContent}</TooltipTrigger>
<TooltipContent className="text-dimmed flex items-center gap-3 py-1.5 pl-2.5 pr-3 text-xs">
<TooltipContent className="flex items-center gap-3 py-1.5 pl-2.5 pr-3 text-xs text-text-bright">
{tooltip} {shortcut && renderShortcutKey()}
</TooltipContent>
</Tooltip>
Expand Down
105 changes: 71 additions & 34 deletions apps/webapp/app/components/primitives/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { EllipsisVerticalIcon } from "@heroicons/react/24/solid";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { DropdownIcon } from "~/assets/icons/DropdownIcon";
import { Link } from "@remix-run/react";
import * as useShortcutKeys from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { type ButtonContentPropsType, LinkButton } from "./Buttons";
import { type ButtonContentPropsType, Button, ButtonContent } from "./Buttons";
import { Paragraph, type ParagraphVariant } from "./Paragraph";
import { ShortcutKey } from "./ShortcutKey";
import { type RenderIcon } from "./Icon";
Expand Down Expand Up @@ -52,42 +53,78 @@ function PopoverSectionHeader({
);
}

function PopoverMenuItem({
to,
icon,
title,
isSelected,
variant = { variant: "small-menu-item" },
leadingIconClassName,
className,
}: {
to: string;
icon?: RenderIcon;
title: React.ReactNode;
isSelected?: boolean;
variant?: ButtonContentPropsType;
leadingIconClassName?: string;
className?: string;
}) {
return (
<LinkButton
to={to}
variant={variant.variant}
LeadingIcon={icon}
leadingIconClassName={leadingIconClassName}
fullWidth
textAlignLeft
TrailingIcon={isSelected ? CheckIcon : undefined}
className={cn(
const PopoverMenuItem = React.forwardRef<
HTMLButtonElement | HTMLAnchorElement,
{
to?: string;
icon?: RenderIcon;
title: React.ReactNode;
isSelected?: boolean;
variant?: ButtonContentPropsType;
leadingIconClassName?: string;
className?: string;
onClick?: React.MouseEventHandler;
disabled?: boolean;
}
>(
(
{
to,
icon,
title,
isSelected,
variant = { variant: "small-menu-item" },
leadingIconClassName,
className,
onClick,
disabled,
},
ref
) => {
const contentProps = {
variant: variant.variant,
LeadingIcon: icon,
leadingIconClassName,
fullWidth: true,
textAlignLeft: true,
TrailingIcon: isSelected ? CheckIcon : undefined,
className: cn(
"group-hover:bg-charcoal-700",
isSelected ? "bg-charcoal-750 group-hover:bg-charcoal-600/50" : undefined,
className
)}
>
{title}
</LinkButton>
);
}
),
} as const;

if (to) {
return (
<Link
to={to}
ref={ref as React.Ref<HTMLAnchorElement>}
className={cn("group/button focus-custom", contentProps.fullWidth ? "w-full" : "")}
onClick={onClick as any}
>
<ButtonContent {...contentProps}>{title}</ButtonContent>
</Link>
);
}

return (
<button
type="button"
ref={ref as React.Ref<HTMLButtonElement>}
onClick={onClick}
disabled={disabled}
className={cn(
"group/button outline-none focus-custom",
contentProps.fullWidth ? "w-full" : ""
)}
>
<ButtonContent {...contentProps}>{title}</ButtonContent>
</button>
);
}
);
PopoverMenuItem.displayName = "PopoverMenuItem";

function PopoverCustomTrigger({
isOpen,
Expand Down
19 changes: 19 additions & 0 deletions apps/webapp/app/presenters/v3/QueueListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ export class QueueListPresenter extends BasePresenter {
name: true,
orderableName: true,
concurrencyLimit: true,
concurrencyLimitBase: true,
concurrencyLimitOverriddenAt: true,
concurrencyLimitOverriddenBy: true,
type: true,
paused: true,
},
Expand All @@ -135,6 +138,17 @@ export class QueueListPresenter extends BasePresenter {
),
]);

// Manually "join" the overridden users because there is no way to implement the relationship
// in prisma without adding a foreign key constraint
const overriddenByIds = queues.map((q) => q.concurrencyLimitOverriddenBy).filter(Boolean);
const overriddenByUsers = await this._replica.user.findMany({
where: {
id: { in: overriddenByIds },
},
});

const overriddenByMap = new Map(overriddenByUsers.map((u) => [u.id, u]));

// Transform queues to include running and queued counts
return queues.map((queue) =>
toQueueItem({
Expand All @@ -144,6 +158,11 @@ export class QueueListPresenter extends BasePresenter {
running: results[1][queue.name] ?? 0,
queued: results[0][queue.name] ?? 0,
concurrencyLimit: queue.concurrencyLimit ?? null,
concurrencyLimitBase: queue.concurrencyLimitBase ?? null,
concurrencyLimitOverriddenAt: queue.concurrencyLimitOverriddenAt ?? null,
concurrencyLimitOverriddenBy: queue.concurrencyLimitOverriddenBy
? overriddenByMap.get(queue.concurrencyLimitOverriddenBy) ?? null
: null,
paused: queue.paused,
})
);
Expand Down
79 changes: 66 additions & 13 deletions apps/webapp/app/presenters/v3/QueueRetrievePresenter.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { engine } from "~/v3/runEngine.server";
import { BasePresenter } from "./basePresenter.server";
import { type TaskQueueType } from "@trigger.dev/database";
import { TaskQueue, User, type TaskQueueType } from "@trigger.dev/database";
import { assertExhaustive } from "@trigger.dev/core";
import { determineEngineVersion } from "~/v3/engineVersion.server";
import { type QueueItem, type RetrieveQueueParam } from "@trigger.dev/core/v3";
import { type Prettify, type QueueItem, type RetrieveQueueParam } from "@trigger.dev/core/v3";
import { PrismaClientOrTransaction } from "@trigger.dev/database";

export type FoundQueue = Prettify<
Omit<TaskQueue, "concurrencyLimitOverriddenBy"> & {
concurrencyLimitOverriddenBy?: User | null;
}
>;

/**
* Shared queue lookup logic used by both QueueRetrievePresenter and PauseQueueService
*/
Expand All @@ -16,22 +22,50 @@ export async function getQueue(
queue: RetrieveQueueParam
) {
if (typeof queue === "string") {
return prismaClient.taskQueue.findFirst({
return joinQueueWithUser(
prismaClient,
await prismaClient.taskQueue.findFirst({
where: {
friendlyId: queue,
runtimeEnvironmentId: environment.id,
},
})
);
}

const queueName =
queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name;
return joinQueueWithUser(
prismaClient,
await prismaClient.taskQueue.findFirst({
where: {
friendlyId: queue,
name: queueName,
runtimeEnvironmentId: environment.id,
},
});
})
);
}

async function joinQueueWithUser(
prismaClient: PrismaClientOrTransaction,
queue?: TaskQueue | null
): Promise<FoundQueue | undefined> {
if (!queue) return undefined;
if (!queue.concurrencyLimitOverriddenBy) {
return {
...queue,
concurrencyLimitOverriddenBy: undefined,
};
}

const queueName =
queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name;
return prismaClient.taskQueue.findFirst({
where: {
name: queueName,
runtimeEnvironmentId: environment.id,
},
const user = await prismaClient.user.findFirst({
where: { id: queue.concurrencyLimitOverriddenBy },
});

return {
...queue,
concurrencyLimitOverriddenBy: user,
};
}

export class QueueRetrievePresenter extends BasePresenter {
Expand Down Expand Up @@ -75,6 +109,9 @@ export class QueueRetrievePresenter extends BasePresenter {
running: results[1]?.[queue.name] ?? 0,
queued: results[0]?.[queue.name] ?? 0,
concurrencyLimit: queue.concurrencyLimit ?? null,
concurrencyLimitBase: queue.concurrencyLimitBase ?? null,
concurrencyLimitOverriddenAt: queue.concurrencyLimitOverriddenAt ?? null,
concurrencyLimitOverriddenBy: queue.concurrencyLimitOverriddenBy ?? null,
paused: queue.paused,
}),
};
Expand Down Expand Up @@ -104,6 +141,9 @@ export function toQueueItem(data: {
running: number;
queued: number;
concurrencyLimit: number | null;
concurrencyLimitBase: number | null;
concurrencyLimitOverriddenAt: Date | null;
concurrencyLimitOverriddenBy: User | null;
paused: boolean;
}): QueueItem & { releaseConcurrencyOnWaitpoint: boolean } {
return {
Expand All @@ -113,9 +153,22 @@ export function toQueueItem(data: {
type: queueTypeFromType(data.type),
running: data.running,
queued: data.queued,
concurrencyLimit: data.concurrencyLimit,
paused: data.paused,
concurrencyLimit: data.concurrencyLimit,
concurrency: {
current: data.concurrencyLimit,
base: data.concurrencyLimitBase,
override: data.concurrencyLimitOverriddenAt ? data.concurrencyLimit : null,
overriddenBy: toQueueConcurrencyOverriddenBy(data.concurrencyLimitOverriddenBy),
overriddenAt: data.concurrencyLimitOverriddenAt,
},
// TODO: This needs to be removed but keeping this here for now to avoid breaking existing clients
releaseConcurrencyOnWaitpoint: true,
};
}

function toQueueConcurrencyOverriddenBy(user: User | null) {
if (!user) return null;

return user.displayName ?? user.name ?? null;
}
Loading