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/metrics/BigNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface BigNumberProps {
valueClassName?: string;
defaultValue?: number;
accessory?: ReactNode;
suffix?: string;
suffix?: ReactNode;
suffixClassName?: string;
compactThreshold?: number;
}
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/components/primitives/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ const allVariants = {
variant: variant,
};

export type ButtonVariant = keyof typeof variant;

export type ButtonContentPropsType = {
children?: React.ReactNode;
LeadingIcon?: RenderIcon;
Expand All @@ -173,7 +175,7 @@ export type ButtonContentPropsType = {
textAlignLeft?: boolean;
className?: string;
shortcut?: ShortcutDefinition;
variant: keyof typeof variant;
variant: ButtonVariant;
shortcutPosition?: "before-trailing-icon" | "after-trailing-icon";
tooltip?: ReactNode;
iconSpacing?: string;
Expand Down
6 changes: 4 additions & 2 deletions apps/webapp/app/components/runs/v3/RunFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1286,8 +1286,10 @@ function VersionsDropdown({
{filtered.length > 0
? filtered.map((version) => (
<SelectItem key={version.version} value={version.version}>
{version.version}{" "}
{version.isCurrent ? <Badge variant="extra-small">current</Badge> : null}
<span className="flex items-center gap-2">
<span className="grow truncate">{version.version}</span>
{version.isCurrent ? <Badge variant="extra-small">Current</Badge> : null}
</span>
</SelectItem>
))
: null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type Environment = {
running: number;
queued: number;
concurrencyLimit: number;
burstFactor: number;
};

export class EnvironmentQueuePresenter extends BasePresenter {
Expand All @@ -26,6 +27,7 @@ export class EnvironmentQueuePresenter extends BasePresenter {
running,
queued,
concurrencyLimit: environment.maximumConcurrencyLimit,
burstFactor: environment.concurrencyLimitBurstFactor.toNumber(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Feedback } from "~/components/Feedback";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BigNumber } from "~/components/metrics/BigNumber";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Button, ButtonVariant, LinkButton } 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";
Expand All @@ -48,6 +48,7 @@ import {
TableRow,
} from "~/components/primitives/Table";
import {
InfoIconTooltip,
SimpleTooltip,
Tooltip,
TooltipContent,
Expand All @@ -65,13 +66,14 @@ import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePrese
import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { docsPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/pathBuilder";
import { docsPath, EnvironmentParamSchema, v3BillingPath, v3RunsPath } from "~/utils/pathBuilder";
import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server";
import { PauseQueueService } from "~/v3/services/pauseQueue.server";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { Header3 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { useThrottle } from "~/hooks/useThrottle";
import { RunsIcon } from "~/assets/icons/RunsIcon";

const SearchParamsSchema = z.object({
query: z.string().optional(),
Expand Down Expand Up @@ -238,6 +240,16 @@ export default function Page() {
}
}, [streamedEvents]);

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 (
<PageContainer>
<NavBar>
Expand All @@ -261,30 +273,63 @@ export default function Page() {
value={environment.queued}
suffix={env.paused && environment.queued > 0 ? "paused" : undefined}
animate
accessory={<EnvironmentPauseResumeButton env={env} />}
accessory={
<div className="flex items-start gap-1">
<LinkButton
variant="tertiary/small"
to={v3RunsPath(organization, project, env, {
statuses: ["PENDING"],
period: "30d",
})}
>
View runs
</LinkButton>
<EnvironmentPauseResumeButton env={env} />
</div>
}
valueClassName={env.paused ? "text-warning" : undefined}
compactThreshold={1000000}
/>
<BigNumber
title="Running"
value={environment.running}
animate
valueClassName={
environment.running === environment.concurrencyLimit ? "text-warning" : undefined
}
valueClassName={limitClassName}
suffix={
environment.running === environment.concurrencyLimit
? "At concurrency limit"
: undefined
limitStatus === "burst" ? (
<span className={cn(limitClassName, "flex items-center gap-1")}>
Including {environment.running - environment.concurrencyLimit} burst runs{" "}
<BurstFactorTooltip environment={environment} />
</span>
) : limitStatus === "limit" ? (
"At concurrency limit"
) : undefined
}
accessory={
<LinkButton
variant="tertiary/small"
to={v3RunsPath(organization, project, env, {
statuses: ["DEQUEUED", "EXECUTING"],
period: "30d",
})}
>
View runs
</LinkButton>
}
compactThreshold={1000000}
/>
<BigNumber
title="Concurrency limit"
value={environment.concurrencyLimit}
animate
valueClassName={
environment.running === environment.concurrencyLimit ? "text-warning" : undefined
valueClassName={limitClassName}
suffix={
environment.burstFactor > 1 ? (
<span className={cn(limitClassName, "flex items-center gap-1")}>
Burst limit {environment.burstFactor * environment.concurrencyLimit}{" "}
<BurstFactorTooltip environment={environment} />
</span>
) : undefined
}
accessory={
plan ? (
Expand Down Expand Up @@ -323,7 +368,14 @@ export default function Page() {
pagination.totalPages > 1 && "grid-rows-[auto_1fr_auto]"
)}
>
<QueueFilters />
<div className="flex items-center gap-2 border-t border-grid-dimmed px-1.5 py-1.5">
<QueueFilters />
<PaginationControls
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
showPageNumbers={false}
/>
</div>
<Table containerClassName="border-t">
<TableHeader>
<TableRow>
Expand Down Expand Up @@ -370,6 +422,9 @@ export default function Page() {
queues.map((queue) => {
const limit = queue.concurrencyLimit ?? environment.concurrencyLimit;
const isAtLimit = queue.running === limit;
const queueFilterableName = `${queue.type === "task" ? "task/" : ""}${
queue.name
}`;
return (
<TableRow key={queue.name}>
<TableCell>
Expand Down Expand Up @@ -450,6 +505,66 @@ export default function Page() {
hiddenButtons={
!queue.paused && <QueuePauseResumeButton queue={queue} />
}
popoverContent={
<>
{queue.paused ? (
<QueuePauseResumeButton
queue={queue}
variant="minimal/small"
fullWidth
showTooltip={false}
/>
) : (
<QueuePauseResumeButton
queue={queue}
variant="minimal/small"
fullWidth
showTooltip={false}
/>
)}
<LinkButton
variant="minimal/small"
to={v3RunsPath(organization, project, env, {
queues: [queueFilterableName],
period: "30d",
})}
fullWidth
textAlignLeft
LeadingIcon={RunsIcon}
leadingIconClassName="text-indigo-500"
>
View all runs
</LinkButton>
<LinkButton
variant="minimal/small"
to={v3RunsPath(organization, project, env, {
queues: [queueFilterableName],
statuses: ["PENDING"],
period: "30d",
})}
fullWidth
textAlignLeft
LeadingIcon={RectangleStackIcon}
leadingIconClassName="text-queues"
>
View queued runs
</LinkButton>
<LinkButton
variant="minimal/small"
to={v3RunsPath(organization, project, env, {
queues: [queueFilterableName],
statuses: ["DEQUEUED", "EXECUTING"],
period: "30d",
})}
fullWidth
textAlignLeft
LeadingIcon={Spinner}
leadingIconClassName="size-4 animate-none"
>
View running runs
</LinkButton>
</>
}
/>
</TableRow>
);
Expand Down Expand Up @@ -603,40 +718,56 @@ function EnvironmentPauseResumeButton({

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 navigation = useNavigation();
const [isOpen, setIsOpen] = useState(false);

const button = (
<Button
type="button"
variant={variant}
LeadingIcon={queue.paused ? PlayIcon : PauseIcon}
leadingIconClassName={queue.paused ? "text-success" : "text-warning"}
fullWidth={fullWidth}
textAlignLeft={fullWidth}
>
{queue.paused ? "Resume..." : "Pause..."}
</Button>
);

const trigger = showTooltip ? (
<div>
<TooltipProvider disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DialogTrigger asChild>{button}</DialogTrigger>
</div>
</TooltipTrigger>
<TooltipContent side="right" className={"text-xs"}>
{queue.paused
? `Resume processing runs in queue "${queue.name}"`
: `Pause processing runs in queue "${queue.name}"`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
) : (
<DialogTrigger asChild>{button}</DialogTrigger>
);

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<div>
<TooltipProvider disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DialogTrigger asChild>
<Button
type="button"
variant="tertiary/small"
LeadingIcon={queue.paused ? PlayIcon : PauseIcon}
leadingIconClassName={queue.paused ? "text-success" : "text-warning"}
>
{queue.paused ? "Resume..." : "Pause..."}
</Button>
</DialogTrigger>
</div>
</TooltipTrigger>
<TooltipContent side="right" className={"text-xs"}>
{queue.paused
? `Resume processing runs in queue "${queue.name}"`
: `Pause processing runs in queue "${queue.name}"`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{trigger}
<DialogContent>
<DialogHeader>{queue.paused ? "Resume queue?" : "Pause queue?"}</DialogHeader>
<div className="flex flex-col gap-3 pt-3">
Expand Down Expand Up @@ -743,7 +874,7 @@ export function QueueFilters() {
const search = searchParams.get("query") ?? "";

return (
<div className="flex w-full border-t border-grid-dimmed px-1.5 py-1.5">
<div className="flex grow">
<Input
name="search"
placeholder="Search queue name"
Expand All @@ -756,3 +887,20 @@ export function QueueFilters() {
</div>
);
}

function BurstFactorTooltip({
environment,
}: {
environment: { burstFactor: number; concurrencyLimit: number };
}) {
return (
<InfoIconTooltip
content={`Your single queue concurrency limit is capped at ${
environment.concurrencyLimit
}, but you can burst up to ${
environment.burstFactor * environment.concurrencyLimit
} when across multiple queues/tasks.`}
contentClassName="max-w-xs"
/>
);
}