diff --git a/apps/webapp/app/components/metrics/BigNumber.tsx b/apps/webapp/app/components/metrics/BigNumber.tsx index 7c4441be34..df3fa9e0a4 100644 --- a/apps/webapp/app/components/metrics/BigNumber.tsx +++ b/apps/webapp/app/components/metrics/BigNumber.tsx @@ -14,7 +14,7 @@ interface BigNumberProps { valueClassName?: string; defaultValue?: number; accessory?: ReactNode; - suffix?: string; + suffix?: ReactNode; suffixClassName?: string; compactThreshold?: number; } diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index bafd772b0a..6859608eef 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -163,6 +163,8 @@ const allVariants = { variant: variant, }; +export type ButtonVariant = keyof typeof variant; + export type ButtonContentPropsType = { children?: React.ReactNode; LeadingIcon?: RenderIcon; @@ -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; diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 9eae1e1eb5..f1153417cd 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -1286,8 +1286,10 @@ function VersionsDropdown({ {filtered.length > 0 ? filtered.map((version) => ( - {version.version}{" "} - {version.isCurrent ? current : null} + + {version.version} + {version.isCurrent ? Current : null} + )) : null} diff --git a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts index 7469a2c0b1..b73c826081 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts @@ -7,6 +7,7 @@ export type Environment = { running: number; queued: number; concurrencyLimit: number; + burstFactor: number; }; export class EnvironmentQueuePresenter extends BasePresenter { @@ -26,6 +27,7 @@ export class EnvironmentQueuePresenter extends BasePresenter { running, queued, concurrencyLimit: environment.maximumConcurrencyLimit, + burstFactor: environment.concurrencyLimitBurstFactor.toNumber(), }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index de6dea2711..13817f9667 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -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"; @@ -48,6 +48,7 @@ import { TableRow, } from "~/components/primitives/Table"; import { + InfoIconTooltip, SimpleTooltip, Tooltip, TooltipContent, @@ -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(), @@ -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 ( @@ -261,7 +273,20 @@ export default function Page() { value={environment.queued} suffix={env.paused && environment.queued > 0 ? "paused" : undefined} animate - accessory={} + accessory={ + + + View runs + + + + } valueClassName={env.paused ? "text-warning" : undefined} compactThreshold={1000000} /> @@ -269,13 +294,27 @@ export default function Page() { 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" ? ( + + Including {environment.running - environment.concurrencyLimit} burst runs{" "} + + + ) : limitStatus === "limit" ? ( + "At concurrency limit" + ) : undefined + } + accessory={ + + View runs + } compactThreshold={1000000} /> @@ -283,8 +322,14 @@ export default function Page() { title="Concurrency limit" value={environment.concurrencyLimit} animate - valueClassName={ - environment.running === environment.concurrencyLimit ? "text-warning" : undefined + valueClassName={limitClassName} + suffix={ + environment.burstFactor > 1 ? ( + + Burst limit {environment.burstFactor * environment.concurrencyLimit}{" "} + + + ) : undefined } accessory={ plan ? ( @@ -323,7 +368,14 @@ export default function Page() { pagination.totalPages > 1 && "grid-rows-[auto_1fr_auto]" )} > - + + + + @@ -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 ( @@ -450,6 +505,66 @@ export default function Page() { hiddenButtons={ !queue.paused && } + popoverContent={ + <> + {queue.paused ? ( + + ) : ( + + )} + + View all runs + + + View queued runs + + + View running runs + + > + } /> ); @@ -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 = ( + + {queue.paused ? "Resume..." : "Pause..."} + + ); + + const trigger = showTooltip ? ( + + + + + + {button} + + + + {queue.paused + ? `Resume processing runs in queue "${queue.name}"` + : `Pause processing runs in queue "${queue.name}"`} + + + + + ) : ( + {button} + ); + return ( - - - - - - - - {queue.paused ? "Resume..." : "Pause..."} - - - - - - {queue.paused - ? `Resume processing runs in queue "${queue.name}"` - : `Pause processing runs in queue "${queue.name}"`} - - - - + {trigger} {queue.paused ? "Resume queue?" : "Pause queue?"} @@ -743,7 +874,7 @@ export function QueueFilters() { const search = searchParams.get("query") ?? ""; return ( - + ); } + +function BurstFactorTooltip({ + environment, +}: { + environment: { burstFactor: number; concurrencyLimit: number }; +}) { + return ( + + ); +}