Skip to content

Waitpoint tokens page, wait.listTokens() and wait.retrieveToken() #1824

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 53 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
16d5013
Added waitpoints/tokens to the sidebar
matt-aitken Mar 20, 2025
ac89e1b
Added indexes to the Waitpoint time for filtering
matt-aitken Mar 20, 2025
e05ee25
Begun work on `WaitpointTokenListPresenter`, the pag is a copy of the…
matt-aitken Mar 20, 2025
964fcf2
MVP of waitpoint token page
matt-aitken Mar 20, 2025
863eb1d
Added status
matt-aitken Mar 20, 2025
11f647f
Expiry of timeout/ttl
matt-aitken Mar 20, 2025
4439880
Improvements to the waitpoint table
matt-aitken Mar 20, 2025
6002c21
Improved columns and icon
matt-aitken Mar 20, 2025
66f1285
Merge remote-tracking branch 'origin/main' into waitpoint-tokens-page
matt-aitken Mar 20, 2025
a41f402
Changes from the RunTag copy on hover branch
matt-aitken Mar 20, 2025
725291f
Fix for nested button error
matt-aitken Mar 20, 2025
f55b059
Added waitpoint tags to the DB/table
matt-aitken Mar 20, 2025
3f6442d
Applied Eric’s task run tag fix (it’s live on prod in the legacy run …
matt-aitken Mar 20, 2025
370c76f
Added tags to waitpoints
matt-aitken Mar 20, 2025
c66d2b2
Removed todos that have been done
matt-aitken Mar 20, 2025
98283fc
Added token support for releaseConcurrency. Also added a ton of JSDocs
matt-aitken Mar 20, 2025
3abad43
Added releaseConcurrency to the API token endpoint…
matt-aitken Mar 20, 2025
35028c2
WIP on waitpoint page filters
matt-aitken Mar 23, 2025
2336a2c
Fix for tags filtering
matt-aitken Mar 23, 2025
bf03eb4
Waitpoint filters working
matt-aitken Mar 23, 2025
0770e5f
Fix for badly named function
matt-aitken Mar 24, 2025
d96924e
WaitpointPresenter used from SpanPresenter
matt-aitken Mar 24, 2025
7825872
Waitpoint detail panel WIP
matt-aitken Mar 24, 2025
86eda3c
Fix for server client hydration issue with CodeBlock
matt-aitken Mar 24, 2025
21f8071
Selected waitpoint panel
matt-aitken Mar 24, 2025
960bd01
Merge remote-tracking branch 'origin/main' into waitpoint-tokens-page
matt-aitken Mar 24, 2025
5bf0cc5
Added a blank state
matt-aitken Mar 24, 2025
9241633
Added waitpoint docs link
matt-aitken Mar 25, 2025
f7c2aa1
Fix for animated number going past the target
matt-aitken Mar 25, 2025
8d4b842
Fix for the queue list pagination and upgrade status
matt-aitken Mar 25, 2025
0819ae7
Engine version error for waitpoint token list
matt-aitken Mar 25, 2025
ca47383
RunTag component doesn’t get squished and hover behaviour is nicer
matt-aitken Mar 25, 2025
4832469
Associating runs with waitpoints
matt-aitken Mar 25, 2025
64d8bbd
Added triggered icon
matt-aitken Mar 25, 2025
6890cce
Link directly to the waitpoint
matt-aitken Mar 25, 2025
d24e976
Fix for TS error on waitpoint retrieve
matt-aitken Mar 25, 2025
e3fffe2
Added CopyableText component, used for waitpoint id in the table
matt-aitken Mar 25, 2025
004ef18
Removed the confetti 🎊
matt-aitken Mar 25, 2025
f8b7141
Deleted some old images
matt-aitken Mar 25, 2025
461e987
Moved some schemas/types to core. Use `id` instead of `friendlyId`
matt-aitken Mar 26, 2025
0efc415
Added wait.listTokens() function. Made some changes to the types to m…
matt-aitken Mar 26, 2025
e17d25c
WIP wait.retrieveToken()
matt-aitken Mar 26, 2025
59011f3
wait.retrieveToken working
matt-aitken Mar 26, 2025
ada673b
Added data to retrieve token
matt-aitken Mar 26, 2025
bf5ec07
Separate ApiWaitpointPresenter completely
matt-aitken Mar 26, 2025
7bc307a
Added completed time to the waitpoint detail panel
matt-aitken Mar 26, 2025
9ad0508
Merge remote-tracking branch 'origin/main' into waitpoint-tokens-page
matt-aitken Mar 26, 2025
fe040df
Fix for the Avatar component having SSR issues. Specify the size in r…
matt-aitken Mar 26, 2025
721558b
Fix for applied idempotency key filter dropdown showing the id field
matt-aitken Mar 26, 2025
0f293a7
Use parentheses to make sure the token list query respects idempotenc…
matt-aitken Mar 26, 2025
bf2fead
Use the proper logger, and have a decent message with info to track t…
matt-aitken Mar 26, 2025
aeb4a58
Pass the org title into the Avatar
matt-aitken Mar 26, 2025
0e4a759
Better error when failing to creating a manual waitpoint after X atte…
matt-aitken Mar 26, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ apps/**/public/build
/packages/cli-v3/src/package.json
.husky
/packages/react-hooks/src/package.json
/packages/core/src/package.json
/packages/trigger-sdk/src/package.json
/packages/python/src/package.json
9 changes: 9 additions & 0 deletions apps/webapp/app/assets/icons/StatusIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { cn } from "~/utils/cn";

export function StatusIcon({ className }: { className?: string }) {
return (
<div className={cn("grid place-items-center", className)}>
<div className="size-[75%] rounded-full border-2 border-text-dimmed" />
</div>
);
}
5 changes: 5 additions & 0 deletions apps/webapp/app/assets/icons/TriggerIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BoltIcon } from "@heroicons/react/20/solid";

export function TriggerIcon({ className }: { className?: string }) {
return <BoltIcon className={className} />;
}
12 changes: 12 additions & 0 deletions apps/webapp/app/assets/icons/WaitpointTokenIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function WaitpointTokenIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
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"
fill="currentColor"
/>
</svg>
);
}
25 changes: 25 additions & 0 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { StepNumber } from "./primitives/StepNumber";
import { TextLink } from "./primitives/TextLink";
import { InitCommandV3, PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands";
import { StepContentContainer } from "./StepContentContainer";
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";

export function HasNoTasksDev() {
return (
Expand Down Expand Up @@ -412,6 +413,30 @@ export function QueuesHasNoTasks() {
);
}

export function NoWaitpointTokens() {
return (
<InfoPanel
title="You don't have any waitpoint tokens"
icon={WaitpointTokenIcon}
iconClassName="text-sky-500"
panelClassName="max-w-md"
accessory={
<LinkButton to={docsPath("wait")} variant="docs/small" LeadingIcon={BookOpenIcon}>
Waitpoint docs
</LinkButton>
}
>
<Paragraph spacing variant="small">
Waitpoint tokens are used to pause runs until you complete the token so the run can
continue.
</Paragraph>
<Paragraph spacing variant="small">
You can build approval workflows using them, as well as other use cases.
</Paragraph>
</InfoPanel>
);
}

function SwitcherPanel() {
const organization = useOrganization();
const project = useProject();
Expand Down
28 changes: 27 additions & 1 deletion apps/webapp/app/components/code/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
import { Clipboard, ClipboardCheck } from "lucide-react";
import type { Language, PrismTheme } from "prism-react-renderer";
import { Highlight, Prism } from "prism-react-renderer";
import { forwardRef, ReactNode, useCallback, useState } from "react";
import { forwardRef, ReactNode, useCallback, useEffect, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "../primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/Dialog";
Expand Down Expand Up @@ -422,6 +422,32 @@ function HighlightCode({
className,
preClassName,
}: HighlightCodeProps) {
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
// This ensures the language definitions are loaded
Promise.all([
//@ts-ignore
import("prismjs/components/prism-json"),
//@ts-ignore
import("prismjs/components/prism-typescript"),
]).then(() => setIsLoaded(true));
}, []);

if (!isLoaded) {
return (
<div
dir="ltr"
className={cn(
"overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
className
)}
>
<pre className={cn("relative mr-2 font-mono leading-relaxed", preClassName)}>{code}</pre>
</div>
);
}

return (
<Highlight theme={theme} code={code} language={language}>
{({
Expand Down
17 changes: 14 additions & 3 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
v3SchedulesPath,
v3TestPath,
v3UsagePath,
v3WaitpointTokensPath,
} from "~/utils/pathBuilder";
import connectedImage from "../../assets/images/cli-connected.png";
import disconnectedImage from "../../assets/images/cli-disconnected.png";
Expand All @@ -80,6 +81,7 @@ import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
import { SideMenuHeader } from "./SideMenuHeader";
import { SideMenuItem } from "./SideMenuItem";
import { SideMenuSection } from "./SideMenuSection";
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";

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

<SideMenuSection title="Waitpoints">
<SideMenuItem
name="Tokens"
icon={WaitpointTokenIcon}
activeIconColor="text-sky-500"
to={v3WaitpointTokensPath(organization, project, environment)}
/>
</SideMenuSection>

<SideMenuSection title="Manage">
<SideMenuItem
name="API keys"
Expand Down Expand Up @@ -293,7 +304,7 @@ function ProjectSelector({
)}
>
<span className="flex items-center gap-1.5 overflow-hidden">
<Avatar avatar={organization.avatar} className="size-5" />
<Avatar avatar={organization.avatar} size={1.25} orgName={organization.title} />
<SelectorDivider />
<span className="truncate text-2sm font-normal text-text-bright">
{project.name ?? "Select a project"}
Expand All @@ -308,7 +319,7 @@ function ProjectSelector({
<div className="flex flex-col gap-2 bg-charcoal-750 p-2">
<div className="flex items-center gap-2.5">
<div className="box-content size-10 overflow-clip rounded-sm bg-charcoal-800">
<Avatar avatar={organization.avatar} className="size-10" includePadding />
<Avatar avatar={organization.avatar} size={2.5} orgName={organization.title} />
</div>
<div className="space-y-0.5">
<Paragraph variant="small/bright">{organization.title}</Paragraph>
Expand Down Expand Up @@ -472,7 +483,7 @@ function SwitchOrganizations({
key={org.id}
to={organizationPath(org)}
title={org.title}
icon={<Avatar className="size-4" avatar={org.avatar} />}
icon={<Avatar size={1} avatar={org.avatar} orgName={org.title} />}
leadingIconClassName="text-text-dimmed"
isSelected={org.id === organization.id}
/>
Expand Down
13 changes: 8 additions & 5 deletions apps/webapp/app/components/primitives/AnimatedNumber.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { motion, useSpring, useTransform } from "framer-motion";
import { motion, useSpring, useTransform, useMotionValue, animate } from "framer-motion";
import { useEffect } from "react";

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

useEffect(() => {
spring.set(value);
}, [spring, value]);
animate(motionValue, value, {
duration: 0.5,
ease: "easeInOut",
});
}, [value]);

return <motion.span>{display}</motion.span>;
}
93 changes: 41 additions & 52 deletions apps/webapp/app/components/primitives/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {
StarIcon,
} from "@heroicons/react/20/solid";
import { type Prisma } from "@trigger.dev/database";
import { useLayoutEffect, useRef, useState } from "react";
import { z } from "zod";
import { useOrganization } from "~/hooks/useOrganizations";
import { logger } from "~/services/logger.server";
import { cn } from "~/utils/cn";

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

export function Avatar({
avatar,
className,
size,
includePadding,
orgName,
}: {
avatar: Avatar;
className?: string;
/** Size in rems of the icon */
size: number;
includePadding?: boolean;
orgName: string;
}) {
switch (avatar.type) {
case "icon":
return <AvatarIcon avatar={avatar} className={className} includePadding={includePadding} />;
return <AvatarIcon avatar={avatar} size={size} includePadding={includePadding} />;
case "letters":
return (
<AvatarLetters avatar={avatar} className={className} includePadding={includePadding} />
<AvatarLetters
avatar={avatar}
size={size}
includePadding={includePadding}
orgName={orgName}
/>
);
case "image":
return <AvatarImage avatar={avatar} className={className} />;
return <AvatarImage avatar={avatar} size={size} />;
}
}

Expand Down Expand Up @@ -101,65 +107,49 @@ export const defaultAvatar: Avatar = {
hex: defaultAvatarHex,
};

function styleFromSize(size: number) {
return {
width: `${size}rem`,
height: `${size}rem`,
};
}

function AvatarLetters({
avatar,
className,
size,
includePadding,
orgName,
}: {
avatar: LettersAvatar;
className?: string;
size: number;
includePadding?: boolean;
orgName: string;
}) {
const organization = useOrganization();
const containerRef = useRef<HTMLSpanElement>(null);
const textRef = useRef<HTMLSpanElement>(null);
const [fontSize, setFontSize] = useState("1rem");

useLayoutEffect(() => {
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth;
// Set font size to 60% of container width (adjust as needed)
setFontSize(`${containerWidth * 0.6}px`);
}

// Optional: Create a ResizeObserver for dynamic resizing
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === containerRef.current) {
const containerWidth = entry.contentRect.width;
setFontSize(`${containerWidth * 0.6}px`);
}
}
});

if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}

return () => {
resizeObserver.disconnect();
};
}, []);

const letters = organization.title.slice(0, 2);

const classes = cn("grid place-items-center", className);
const letters = orgName.slice(0, 2);

const style = {
backgroundColor: avatar.hex,
};

const scaleFactor = includePadding ? 0.8 : 1;

return (
<span className={cn("grid place-items-center overflow-hidden text-charcoal-750", classes)}>
<span
className="grid place-items-center overflow-hidden text-charcoal-750"
style={styleFromSize(size)}
>
{/* This is the square container */}
<span
ref={containerRef}
className={cn(
"relative grid place-items-center overflow-hidden rounded-[10%] font-semibold",
includePadding ? "size-[80%]" : "size-[100%]"
)}
style={style}
>
<span ref={textRef} className="font-bold leading-none" style={{ fontSize }}>
<span
className="font-bold leading-none"
style={{ fontSize: `${size * 0.6 * scaleFactor}rem` }}
>
{letters}
</span>
</span>
Expand All @@ -169,29 +159,28 @@ function AvatarLetters({

function AvatarIcon({
avatar,
className,
size,
includePadding,
}: {
avatar: IconAvatar;
className?: string;
size: number;
includePadding?: boolean;
}) {
const classes = cn("aspect-square", className);
const style = {
color: avatar.hex,
};

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

function AvatarImage({ avatar, className }: { avatar: ImageAvatar; className?: string }) {
function AvatarImage({ avatar, size }: { avatar: ImageAvatar; size: number }) {
return (
<span className="grid place-items-center">
<span className="grid place-items-center" style={styleFromSize(size)}>
<img src={avatar.url} alt="Organization avatar" className="size-6" />
</span>
);
Expand Down
Loading