Skip to content

Commit 00586ff

Browse files
authored
Waitpoint tokens page, wait.listTokens() and wait.retrieveToken() (#1824)
* Added waitpoints/tokens to the sidebar * Added indexes to the Waitpoint time for filtering * Begun work on `WaitpointTokenListPresenter`, the pag is a copy of the Queues page for now * MVP of waitpoint token page * Added status * Expiry of timeout/ttl * Improvements to the waitpoint table * Improved columns and icon * Changes from the RunTag copy on hover branch * Fix for nested button error * Added waitpoint tags to the DB/table * Applied Eric’s task run tag fix (it’s live on prod in the legacy run engine branch) * Added tags to waitpoints * Removed todos that have been done * Added token support for releaseConcurrency. Also added a ton of JSDocs * Added releaseConcurrency to the API token endpoint… * WIP on waitpoint page filters * Fix for tags filtering * Waitpoint filters working * Fix for badly named function * WaitpointPresenter used from SpanPresenter * Waitpoint detail panel WIP * Fix for server client hydration issue with CodeBlock * Selected waitpoint panel * Added a blank state * Added waitpoint docs link * Fix for animated number going past the target * Fix for the queue list pagination and upgrade status * Engine version error for waitpoint token list * RunTag component doesn’t get squished and hover behaviour is nicer * Associating runs with waitpoints * Added triggered icon * Link directly to the waitpoint * Fix for TS error on waitpoint retrieve * Added CopyableText component, used for waitpoint id in the table * Removed the confetti 🎊 * Deleted some old images * Moved some schemas/types to core. Use `id` instead of `friendlyId` * Added wait.listTokens() function. Made some changes to the types to make it nicer * WIP wait.retrieveToken() * wait.retrieveToken working * Added data to retrieve token * Separate ApiWaitpointPresenter completely * Added completed time to the waitpoint detail panel * Fix for the Avatar component having SSR issues. Specify the size in rems and removed the useLayoutEffect * Fix for applied idempotency key filter dropdown showing the id field * Use parentheses to make sure the token list query respects idempotency key correctly * Use the proper logger, and have a decent message with info to track the bug down * Pass the org title into the Avatar * Better error when failing to creating a manual waitpoint after X attempts
1 parent c08c3b4 commit 00586ff

File tree

63 files changed

+3278
-489
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3278
-489
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ apps/**/public/build
6060
/packages/cli-v3/src/package.json
6161
.husky
6262
/packages/react-hooks/src/package.json
63+
/packages/core/src/package.json
64+
/packages/trigger-sdk/src/package.json
65+
/packages/python/src/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { cn } from "~/utils/cn";
2+
3+
export function StatusIcon({ className }: { className?: string }) {
4+
return (
5+
<div className={cn("grid place-items-center", className)}>
6+
<div className="size-[75%] rounded-full border-2 border-text-dimmed" />
7+
</div>
8+
);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { BoltIcon } from "@heroicons/react/20/solid";
2+
3+
export function TriggerIcon({ className }: { className?: string }) {
4+
return <BoltIcon className={className} />;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function WaitpointTokenIcon({ className }: { className?: string }) {
2+
return (
3+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
fillRule="evenodd"
6+
clipRule="evenodd"
7+
d="M6.71193 3.50338C6.89005 3.1921 7.22123 3.00004 7.57987 3.00003L16.4201 3C16.7787 3 17.1099 3.19206 17.288 3.50334L21.8658 11.5034C22.0419 11.8111 22.0419 12.189 21.8658 12.4967L17.288 20.4967C17.1099 20.8079 16.7787 21 16.4201 21H7.57987C7.22123 21 6.89005 20.8079 6.71193 20.4967L2.1342 12.4967C1.95813 12.189 1.95813 11.8111 2.1342 11.5034L6.71193 3.50338ZM8.5 9.00011C8.5 8.44783 8.94771 8.00011 9.5 8.00011H10C10.5523 8.00011 11 8.44783 11 9.00011V15.0001C11 15.5524 10.5523 16.0001 10 16.0001H9.5C8.94771 16.0001 8.5 15.5524 8.5 15.0001V9.00011ZM14 8.00006C13.4477 8.00006 13 8.44777 13 9.00006V15.0001C13 15.5523 13.4477 16.0001 14 16.0001H14.5C15.0523 16.0001 15.5 15.5523 15.5 15.0001V9.00006C15.5 8.44777 15.0523 8.00006 14.5 8.00006H14Z"
8+
fill="currentColor"
9+
/>
10+
</svg>
11+
);
12+
}

apps/webapp/app/components/BlankStatePanels.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { StepNumber } from "./primitives/StepNumber";
3535
import { TextLink } from "./primitives/TextLink";
3636
import { InitCommandV3, PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands";
3737
import { StepContentContainer } from "./StepContentContainer";
38+
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
3839

3940
export function HasNoTasksDev() {
4041
return (
@@ -412,6 +413,30 @@ export function QueuesHasNoTasks() {
412413
);
413414
}
414415

416+
export function NoWaitpointTokens() {
417+
return (
418+
<InfoPanel
419+
title="You don't have any waitpoint tokens"
420+
icon={WaitpointTokenIcon}
421+
iconClassName="text-sky-500"
422+
panelClassName="max-w-md"
423+
accessory={
424+
<LinkButton to={docsPath("wait")} variant="docs/small" LeadingIcon={BookOpenIcon}>
425+
Waitpoint docs
426+
</LinkButton>
427+
}
428+
>
429+
<Paragraph spacing variant="small">
430+
Waitpoint tokens are used to pause runs until you complete the token so the run can
431+
continue.
432+
</Paragraph>
433+
<Paragraph spacing variant="small">
434+
You can build approval workflows using them, as well as other use cases.
435+
</Paragraph>
436+
</InfoPanel>
437+
);
438+
}
439+
415440
function SwitcherPanel() {
416441
const organization = useOrganization();
417442
const project = useProject();

apps/webapp/app/components/code/CodeBlock.tsx

+27-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
22
import { Clipboard, ClipboardCheck } from "lucide-react";
33
import type { Language, PrismTheme } from "prism-react-renderer";
44
import { Highlight, Prism } from "prism-react-renderer";
5-
import { forwardRef, ReactNode, useCallback, useState } from "react";
5+
import { forwardRef, ReactNode, useCallback, useEffect, useState } from "react";
66
import { cn } from "~/utils/cn";
77
import { Button } from "../primitives/Buttons";
88
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/Dialog";
@@ -422,6 +422,32 @@ function HighlightCode({
422422
className,
423423
preClassName,
424424
}: HighlightCodeProps) {
425+
const [isLoaded, setIsLoaded] = useState(false);
426+
427+
useEffect(() => {
428+
// This ensures the language definitions are loaded
429+
Promise.all([
430+
//@ts-ignore
431+
import("prismjs/components/prism-json"),
432+
//@ts-ignore
433+
import("prismjs/components/prism-typescript"),
434+
]).then(() => setIsLoaded(true));
435+
}, []);
436+
437+
if (!isLoaded) {
438+
return (
439+
<div
440+
dir="ltr"
441+
className={cn(
442+
"overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
443+
className
444+
)}
445+
>
446+
<pre className={cn("relative mr-2 font-mono leading-relaxed", preClassName)}>{code}</pre>
447+
</div>
448+
);
449+
}
450+
425451
return (
426452
<Highlight theme={theme} code={code} language={language}>
427453
{({

apps/webapp/app/components/navigation/SideMenu.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
v3SchedulesPath,
5555
v3TestPath,
5656
v3UsagePath,
57+
v3WaitpointTokensPath,
5758
} from "~/utils/pathBuilder";
5859
import connectedImage from "../../assets/images/cli-connected.png";
5960
import disconnectedImage from "../../assets/images/cli-disconnected.png";
@@ -80,6 +81,7 @@ import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
8081
import { SideMenuHeader } from "./SideMenuHeader";
8182
import { SideMenuItem } from "./SideMenuItem";
8283
import { SideMenuSection } from "./SideMenuSection";
84+
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
8385

8486
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
8587
export type SideMenuProject = Pick<
@@ -211,6 +213,15 @@ export function SideMenu({
211213
/>
212214
</div>
213215

216+
<SideMenuSection title="Waitpoints">
217+
<SideMenuItem
218+
name="Tokens"
219+
icon={WaitpointTokenIcon}
220+
activeIconColor="text-sky-500"
221+
to={v3WaitpointTokensPath(organization, project, environment)}
222+
/>
223+
</SideMenuSection>
224+
214225
<SideMenuSection title="Manage">
215226
<SideMenuItem
216227
name="API keys"
@@ -293,7 +304,7 @@ function ProjectSelector({
293304
)}
294305
>
295306
<span className="flex items-center gap-1.5 overflow-hidden">
296-
<Avatar avatar={organization.avatar} className="size-5" />
307+
<Avatar avatar={organization.avatar} size={1.25} orgName={organization.title} />
297308
<SelectorDivider />
298309
<span className="truncate text-2sm font-normal text-text-bright">
299310
{project.name ?? "Select a project"}
@@ -308,7 +319,7 @@ function ProjectSelector({
308319
<div className="flex flex-col gap-2 bg-charcoal-750 p-2">
309320
<div className="flex items-center gap-2.5">
310321
<div className="box-content size-10 overflow-clip rounded-sm bg-charcoal-800">
311-
<Avatar avatar={organization.avatar} className="size-10" includePadding />
322+
<Avatar avatar={organization.avatar} size={2.5} orgName={organization.title} />
312323
</div>
313324
<div className="space-y-0.5">
314325
<Paragraph variant="small/bright">{organization.title}</Paragraph>
@@ -472,7 +483,7 @@ function SwitchOrganizations({
472483
key={org.id}
473484
to={organizationPath(org)}
474485
title={org.title}
475-
icon={<Avatar className="size-4" avatar={org.avatar} />}
486+
icon={<Avatar size={1} avatar={org.avatar} orgName={org.title} />}
476487
leadingIconClassName="text-text-dimmed"
477488
isSelected={org.id === organization.id}
478489
/>
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { motion, useSpring, useTransform } from "framer-motion";
1+
import { motion, useSpring, useTransform, useMotionValue, animate } from "framer-motion";
22
import { useEffect } from "react";
33

44
export function AnimatedNumber({ value }: { value: number }) {
5-
let spring = useSpring(value, { mass: 0.8, stiffness: 75, damping: 15 });
6-
let display = useTransform(spring, (current) => Math.round(current).toLocaleString());
5+
const motionValue = useMotionValue(value);
6+
let display = useTransform(motionValue, (current) => Math.round(current).toLocaleString());
77

88
useEffect(() => {
9-
spring.set(value);
10-
}, [spring, value]);
9+
animate(motionValue, value, {
10+
duration: 0.5,
11+
ease: "easeInOut",
12+
});
13+
}, [value]);
1114

1215
return <motion.span>{display}</motion.span>;
1316
}

apps/webapp/app/components/primitives/Avatar.tsx

+41-52
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
StarIcon,
88
} from "@heroicons/react/20/solid";
99
import { type Prisma } from "@trigger.dev/database";
10-
import { useLayoutEffect, useRef, useState } from "react";
1110
import { z } from "zod";
12-
import { useOrganization } from "~/hooks/useOrganizations";
1311
import { logger } from "~/services/logger.server";
1412
import { cn } from "~/utils/cn";
1513

@@ -53,22 +51,30 @@ export function parseAvatar(json: Prisma.JsonValue, defaultAvatar: Avatar): Avat
5351

5452
export function Avatar({
5553
avatar,
56-
className,
54+
size,
5755
includePadding,
56+
orgName,
5857
}: {
5958
avatar: Avatar;
60-
className?: string;
59+
/** Size in rems of the icon */
60+
size: number;
6161
includePadding?: boolean;
62+
orgName: string;
6263
}) {
6364
switch (avatar.type) {
6465
case "icon":
65-
return <AvatarIcon avatar={avatar} className={className} includePadding={includePadding} />;
66+
return <AvatarIcon avatar={avatar} size={size} includePadding={includePadding} />;
6667
case "letters":
6768
return (
68-
<AvatarLetters avatar={avatar} className={className} includePadding={includePadding} />
69+
<AvatarLetters
70+
avatar={avatar}
71+
size={size}
72+
includePadding={includePadding}
73+
orgName={orgName}
74+
/>
6975
);
7076
case "image":
71-
return <AvatarImage avatar={avatar} className={className} />;
77+
return <AvatarImage avatar={avatar} size={size} />;
7278
}
7379
}
7480

@@ -101,65 +107,49 @@ export const defaultAvatar: Avatar = {
101107
hex: defaultAvatarHex,
102108
};
103109

110+
function styleFromSize(size: number) {
111+
return {
112+
width: `${size}rem`,
113+
height: `${size}rem`,
114+
};
115+
}
116+
104117
function AvatarLetters({
105118
avatar,
106-
className,
119+
size,
107120
includePadding,
121+
orgName,
108122
}: {
109123
avatar: LettersAvatar;
110-
className?: string;
124+
size: number;
111125
includePadding?: boolean;
126+
orgName: string;
112127
}) {
113-
const organization = useOrganization();
114-
const containerRef = useRef<HTMLSpanElement>(null);
115-
const textRef = useRef<HTMLSpanElement>(null);
116-
const [fontSize, setFontSize] = useState("1rem");
117-
118-
useLayoutEffect(() => {
119-
if (containerRef.current) {
120-
const containerWidth = containerRef.current.offsetWidth;
121-
// Set font size to 60% of container width (adjust as needed)
122-
setFontSize(`${containerWidth * 0.6}px`);
123-
}
124-
125-
// Optional: Create a ResizeObserver for dynamic resizing
126-
const resizeObserver = new ResizeObserver((entries) => {
127-
for (const entry of entries) {
128-
if (entry.target === containerRef.current) {
129-
const containerWidth = entry.contentRect.width;
130-
setFontSize(`${containerWidth * 0.6}px`);
131-
}
132-
}
133-
});
134-
135-
if (containerRef.current) {
136-
resizeObserver.observe(containerRef.current);
137-
}
138-
139-
return () => {
140-
resizeObserver.disconnect();
141-
};
142-
}, []);
143-
144-
const letters = organization.title.slice(0, 2);
145-
146-
const classes = cn("grid place-items-center", className);
128+
const letters = orgName.slice(0, 2);
129+
147130
const style = {
148131
backgroundColor: avatar.hex,
149132
};
150133

134+
const scaleFactor = includePadding ? 0.8 : 1;
135+
151136
return (
152-
<span className={cn("grid place-items-center overflow-hidden text-charcoal-750", classes)}>
137+
<span
138+
className="grid place-items-center overflow-hidden text-charcoal-750"
139+
style={styleFromSize(size)}
140+
>
153141
{/* This is the square container */}
154142
<span
155-
ref={containerRef}
156143
className={cn(
157144
"relative grid place-items-center overflow-hidden rounded-[10%] font-semibold",
158145
includePadding ? "size-[80%]" : "size-[100%]"
159146
)}
160147
style={style}
161148
>
162-
<span ref={textRef} className="font-bold leading-none" style={{ fontSize }}>
149+
<span
150+
className="font-bold leading-none"
151+
style={{ fontSize: `${size * 0.6 * scaleFactor}rem` }}
152+
>
163153
{letters}
164154
</span>
165155
</span>
@@ -169,29 +159,28 @@ function AvatarLetters({
169159

170160
function AvatarIcon({
171161
avatar,
172-
className,
162+
size,
173163
includePadding,
174164
}: {
175165
avatar: IconAvatar;
176-
className?: string;
166+
size: number;
177167
includePadding?: boolean;
178168
}) {
179-
const classes = cn("aspect-square", className);
180169
const style = {
181170
color: avatar.hex,
182171
};
183172

184173
const IconComponent = avatarIcons[avatar.name];
185174
return (
186-
<span className={cn("grid place-items-center", classes)}>
175+
<span className="grid aspect-square place-items-center" style={styleFromSize(size)}>
187176
<IconComponent className={includePadding ? "size-[80%]" : "size-[100%]"} style={style} />
188177
</span>
189178
);
190179
}
191180

192-
function AvatarImage({ avatar, className }: { avatar: ImageAvatar; className?: string }) {
181+
function AvatarImage({ avatar, size }: { avatar: ImageAvatar; size: number }) {
193182
return (
194-
<span className="grid place-items-center">
183+
<span className="grid place-items-center" style={styleFromSize(size)}>
195184
<img src={avatar.url} alt="Organization avatar" className="size-6" />
196185
</span>
197186
);

0 commit comments

Comments
 (0)