Highlight queues when concurrency limit reached and new table col (#2008)
* Combines the Running and Concurrency limit cols into 1 * Display a badge when a queue is at the concurrency limit * Colors the Running/Limit column text amber if the concurrency limit is hit * Turns the “Running” big number amber and shows “At concurrency limit” text * BigNumber now handles big values using formatNumber and formatNumberCompact Also includes some responsive improvements to make sure things wrap when it gets tight * Adds a new col for showing how the queue is limited * Reinstates a threshold for making very big numbers compact * Added border to make the search bar not float
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { AnimatedNumber } from "../primitives/AnimatedNumber";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { formatNumber, formatNumberCompact } from "~/utils/numberFormatter";
|
||||
import { Header3 } from "../primitives/Headers";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { AnimatedNumber } from "../primitives/AnimatedNumber";
|
||||
|
||||
interface BigNumberProps {
|
||||
title: ReactNode;
|
||||
@@ -13,6 +16,7 @@ interface BigNumberProps {
|
||||
accessory?: ReactNode;
|
||||
suffix?: string;
|
||||
suffixClassName?: string;
|
||||
compactThreshold?: number;
|
||||
}
|
||||
|
||||
export function BigNumber({
|
||||
@@ -25,25 +29,39 @@ export function BigNumber({
|
||||
accessory,
|
||||
animate = false,
|
||||
loading = false,
|
||||
compactThreshold,
|
||||
}: BigNumberProps) {
|
||||
const v = value ?? defaultValue;
|
||||
|
||||
const shouldCompact =
|
||||
typeof compactThreshold === "number" && v !== undefined && v >= compactThreshold;
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[1.5rem_auto] gap-4 rounded-sm border border-grid-dimmed bg-background-bright p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-2sm text-text-dimmed">{title}</div>
|
||||
<div className="flex flex-col justify-between gap-4 rounded-sm border border-grid-dimmed bg-background-bright p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<Header3 className="leading-6">{title}</Header3>
|
||||
{accessory && <div className="flex-shrink-0">{accessory}</div>}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-[3.75rem] text-[3.75rem] font-normal tabular-nums leading-none text-text-bright",
|
||||
"text-[3.75rem] font-normal tabular-nums leading-none text-text-bright",
|
||||
valueClassName
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner className="size-6" />
|
||||
) : v !== undefined ? (
|
||||
<div className="flex items-baseline gap-1">
|
||||
{animate ? <AnimatedNumber value={v} /> : v}
|
||||
<div className="flex flex-wrap items-baseline gap-2">
|
||||
{shouldCompact ? (
|
||||
<SimpleTooltip
|
||||
button={animate ? <AnimatedNumber value={v} /> : formatNumberCompact(v)}
|
||||
content={formatNumber(v)}
|
||||
/>
|
||||
) : animate ? (
|
||||
<AnimatedNumber value={v} />
|
||||
) : (
|
||||
formatNumber(v)
|
||||
)}
|
||||
{suffix && <div className={cn("text-xs", suffixClassName)}>{suffix}</div>}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { motion, useSpring, useTransform, useMotionValue, animate } from "framer-motion";
|
||||
import { animate, motion, useMotionValue, useTransform } from "framer-motion";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function AnimatedNumber({ value }: { value: number }) {
|
||||
|
||||
+151
-81
@@ -69,6 +69,7 @@ import { docsPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/pathBui
|
||||
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";
|
||||
|
||||
@@ -257,13 +258,30 @@ export default function Page() {
|
||||
suffix={env.paused && environment.queued > 0 ? "paused" : undefined}
|
||||
animate
|
||||
accessory={<EnvironmentPauseResumeButton env={env} />}
|
||||
valueClassName={env.paused ? "text-amber-500" : undefined}
|
||||
valueClassName={env.paused ? "text-warning" : undefined}
|
||||
compactThreshold={1000000}
|
||||
/>
|
||||
<BigNumber
|
||||
title="Running"
|
||||
value={environment.running}
|
||||
animate
|
||||
valueClassName={
|
||||
environment.running === environment.concurrencyLimit ? "text-warning" : undefined
|
||||
}
|
||||
suffix={
|
||||
environment.running === environment.concurrencyLimit
|
||||
? "At concurrency limit"
|
||||
: undefined
|
||||
}
|
||||
compactThreshold={1000000}
|
||||
/>
|
||||
<BigNumber title="Running" value={environment.running} animate />
|
||||
<BigNumber
|
||||
title="Concurrency limit"
|
||||
value={environment.concurrencyLimit}
|
||||
animate
|
||||
valueClassName={
|
||||
environment.running === environment.concurrencyLimit ? "text-warning" : undefined
|
||||
}
|
||||
accessory={
|
||||
plan ? (
|
||||
plan?.v3Subscription?.plan?.limits.concurrentRuns.canExceed ? (
|
||||
@@ -307,7 +325,37 @@ export default function Page() {
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Queued</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Running</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Running/limit</TableHeaderCell>
|
||||
<TableHeaderCell
|
||||
alignment="right"
|
||||
tooltip={
|
||||
<div className="max-w-xs space-y-2 p-1 text-left">
|
||||
<div className="space-y-0.5">
|
||||
<Header3>Environment</Header3>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className="!text-wrap text-text-dimmed"
|
||||
spacing
|
||||
>
|
||||
This queue is limited by your environment's concurrency limit of{" "}
|
||||
{environment.concurrencyLimit}.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<Header3>User</Header3>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className="!text-wrap text-text-dimmed"
|
||||
spacing
|
||||
>
|
||||
This queue is limited by a concurrency limit set in your code.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Limited by
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell
|
||||
alignment="right"
|
||||
tooltip={
|
||||
@@ -334,7 +382,6 @@ export default function Page() {
|
||||
>
|
||||
Release on waitpoint
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Concurrency limit</TableHeaderCell>
|
||||
<TableHeaderCell className="w-[1%] pl-24">
|
||||
<span className="sr-only">Pause/resume</span>
|
||||
</TableHeaderCell>
|
||||
@@ -342,80 +389,103 @@ export default function Page() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{queues.length > 0 ? (
|
||||
queues.map((queue) => (
|
||||
<TableRow key={queue.name}>
|
||||
<TableCell>
|
||||
<span className="flex items-center gap-2">
|
||||
{queue.type === "task" ? (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<TaskIconSmall
|
||||
className={cn(
|
||||
"size-[1.125rem] text-blue-500",
|
||||
queue.paused && "opacity-50"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
content={`This queue was automatically created from your "${queue.name}" task`}
|
||||
/>
|
||||
) : (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<RectangleStackIcon
|
||||
className={cn(
|
||||
"size-[1.125rem] text-purple-500",
|
||||
queue.paused && "opacity-50"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
content={`This is a custom queue you added in your code.`}
|
||||
/>
|
||||
queues.map((queue) => {
|
||||
const limit = queue.concurrencyLimit ?? environment.concurrencyLimit;
|
||||
const isAtLimit = queue.running === limit;
|
||||
return (
|
||||
<TableRow key={queue.name}>
|
||||
<TableCell>
|
||||
<span className="flex items-center gap-2">
|
||||
{queue.type === "task" ? (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<TaskIconSmall
|
||||
className={cn(
|
||||
"size-[1.125rem] text-blue-500",
|
||||
queue.paused && "opacity-50"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
content={`This queue was automatically created from your "${queue.name}" task`}
|
||||
/>
|
||||
) : (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<RectangleStackIcon
|
||||
className={cn(
|
||||
"size-[1.125rem] text-purple-500",
|
||||
queue.paused && "opacity-50"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
content={`This is a custom queue you added in your code.`}
|
||||
/>
|
||||
)}
|
||||
<span className={queue.paused ? "opacity-50" : undefined}>
|
||||
{queue.name}
|
||||
</span>
|
||||
{queue.paused ? (
|
||||
<Badge variant="extra-small" className="text-warning">
|
||||
Paused
|
||||
</Badge>
|
||||
) : null}
|
||||
{isAtLimit ? (
|
||||
<Badge variant="extra-small" className="text-warning">
|
||||
At concurrency limit
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={queue.paused ? "opacity-50" : undefined}
|
||||
>
|
||||
{queue.queued}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={cn(
|
||||
queue.paused ? "tabular-nums opacity-50" : undefined,
|
||||
isAtLimit && "text-warning"
|
||||
)}
|
||||
<span className={queue.paused ? "opacity-50" : undefined}>
|
||||
{queue.name}
|
||||
>
|
||||
{queue.running}/
|
||||
<span
|
||||
className={cn(
|
||||
"tabular-nums text-text-dimmed",
|
||||
isAtLimit && "text-warning"
|
||||
)}
|
||||
>
|
||||
{limit}
|
||||
</span>
|
||||
{queue.paused ? (
|
||||
<Badge variant="extra-small" className="text-warning">
|
||||
Paused
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={queue.paused ? "opacity-50" : undefined}
|
||||
>
|
||||
{queue.queued}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={queue.paused ? "opacity-50" : undefined}
|
||||
>
|
||||
{queue.running}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={queue.paused ? "opacity-50" : undefined}
|
||||
>
|
||||
{queue.releaseConcurrencyOnWaitpoint ? "Yes" : "No"}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={queue.paused ? "opacity-50" : undefined}
|
||||
>
|
||||
{queue.concurrencyLimit ?? (
|
||||
<span className="text-text-dimmed">
|
||||
Max ({environment.concurrencyLimit})
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCellMenu
|
||||
isSticky
|
||||
visibleButtons={queue.paused && <QueuePauseResumeButton queue={queue} />}
|
||||
hiddenButtons={!queue.paused && <QueuePauseResumeButton queue={queue} />}
|
||||
/>
|
||||
</TableRow>
|
||||
))
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={cn(
|
||||
queue.paused ? "opacity-50" : undefined,
|
||||
isAtLimit && "text-warning"
|
||||
)}
|
||||
>
|
||||
{queue.concurrencyLimit ? "User" : "Environment"}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
alignment="right"
|
||||
className={queue.paused ? "opacity-50" : undefined}
|
||||
>
|
||||
{queue.releaseConcurrencyOnWaitpoint ? "Yes" : "No"}
|
||||
</TableCell>
|
||||
<TableCellMenu
|
||||
isSticky
|
||||
visibleButtons={
|
||||
queue.paused && <QueuePauseResumeButton queue={queue} />
|
||||
}
|
||||
hiddenButtons={
|
||||
!queue.paused && <QueuePauseResumeButton queue={queue} />
|
||||
}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
@@ -503,7 +573,7 @@ function EnvironmentPauseResumeButton({
|
||||
type="button"
|
||||
variant="secondary/small"
|
||||
LeadingIcon={env.paused ? PlayIcon : PauseIcon}
|
||||
leadingIconClassName={env.paused ? "text-success" : "text-amber-500"}
|
||||
leadingIconClassName={env.paused ? "text-success" : "text-warning"}
|
||||
>
|
||||
{env.paused ? "Resume..." : "Pause environment..."}
|
||||
</Button>
|
||||
@@ -512,8 +582,8 @@ function EnvironmentPauseResumeButton({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={"text-xs"}>
|
||||
{env.paused
|
||||
? `Resume processing runs in ${environmentFullTitle(env)}.`
|
||||
: `Pause processing runs in ${environmentFullTitle(env)}.`}
|
||||
? `Resume processing runs in ${environmentFullTitle(env)}`
|
||||
: `Pause processing runs in ${environmentFullTitle(env)}`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -582,7 +652,7 @@ function QueuePauseResumeButton({
|
||||
type="button"
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={queue.paused ? PlayIcon : PauseIcon}
|
||||
leadingIconClassName={queue.paused ? "text-success" : "text-amber-500"}
|
||||
leadingIconClassName={queue.paused ? "text-success" : "text-warning"}
|
||||
>
|
||||
{queue.paused ? "Resume..." : "Pause..."}
|
||||
</Button>
|
||||
@@ -703,7 +773,7 @@ export function QueueFilters() {
|
||||
const search = searchParams.get("query") ?? "";
|
||||
|
||||
return (
|
||||
<div className="flex w-full px-3 pb-3">
|
||||
<div className="flex w-full border-t border-grid-dimmed px-1.5 py-1.5">
|
||||
<Input
|
||||
name="search"
|
||||
placeholder="Search queue name"
|
||||
|
||||
Reference in New Issue
Block a user