Skip to content

Commit 45e40a8

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
feat(webapp): webhooks beta access page and side menu badge cleanup
Webhooks now appear in the dashboard side menu with a Beta badge, and organizations without access can request beta access from the Webhooks page. The New badges on Sessions, Prompts and Models are gone. Mono-RevId: 84bc61a8f9d50fc1bcc13e303c768ecf07dc8982
1 parent a3869e8 commit 45e40a8

10 files changed

Lines changed: 573 additions & 12 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Webhooks now appear in the dashboard side menu with a Beta badge, and organizations without access can request beta access from the Webhooks page. The New badges on Sessions, Prompts and Models are gone.

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ import {
9696
} from "~/utils/pathBuilder";
9797
import { FreePlanUsage } from "../billing/FreePlanUsage";
9898
import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence";
99-
import { NewBadge } from "../FeatureBadges";
99+
import { BetaBadge } from "../FeatureBadges";
100100
import { Button, LinkButton } from "../primitives/Buttons";
101101
import { Dialog, DialogTrigger } from "../primitives/Dialog";
102102
import { type RenderIcon } from "../primitives/Icon";
@@ -961,19 +961,21 @@ export function SideMenu({
961961
inactiveIconColor="text-text-dimmed"
962962
to={v3SessionsPath(organization, project, environment)}
963963
data-action="sessions"
964-
badge={<NewBadge />}
965964
isCollapsed={isCollapsed}
966965
yieldActiveToFavorite
967966
/>
968-
{(user.admin || user.isImpersonating || featureFlags.hasWebhooksAccess) && (
967+
{(isManagedCloud ||
968+
user.admin ||
969+
user.isImpersonating ||
970+
featureFlags.hasWebhooksAccess) && (
969971
<SideMenuItem
970972
name="Webhooks"
971973
icon={WebhookIcon}
972974
activeIconColor="text-webhooks"
973975
inactiveIconColor="text-text-dimmed"
974976
to={v3WebhooksPath(organization, project, environment)}
975977
data-action="webhooks"
976-
badge={<NewBadge />}
978+
badge={<BetaBadge />}
977979
isCollapsed={isCollapsed}
978980
yieldActiveToFavorite
979981
/>

‎apps/webapp/app/components/navigation/sideMenuSections.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
concurrencyPath,
4343
v3WaitpointTokensPath,
4444
} from "~/utils/pathBuilder";
45-
import { AlphaBadge, NewBadge } from "../FeatureBadges";
45+
import { AlphaBadge } from "../FeatureBadges";
4646
import { type RenderIcon } from "../primitives/Icon";
4747
import { type SideMenuSectionId } from "./sideMenuTypes";
4848

@@ -107,7 +107,6 @@ export function buildSideMenuSections({
107107
activeIconColor: "text-aiPrompts",
108108
to: v3PromptsPath(organization, project, environment),
109109
dataAction: "prompts",
110-
badge: <NewBadge />,
111110
},
112111
{
113112
id: "models",
@@ -116,7 +115,6 @@ export function buildSideMenuSections({
116115
activeIconColor: "text-models",
117116
to: v3ModelsPath(organization, project, environment),
118117
dataAction: "models",
119-
badge: <NewBadge />,
120118
},
121119
],
122120
});
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import {
2+
BoltIcon,
3+
ChatBubbleLeftRightIcon,
4+
CheckIcon,
5+
ClockIcon,
6+
CodeBracketIcon,
7+
PauseCircleIcon,
8+
ShieldCheckIcon,
9+
} from "@heroicons/react/20/solid";
10+
import { useFetcher } from "@remix-run/react";
11+
import { type ComponentType, type ReactNode } from "react";
12+
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
13+
import { CodeBlock } from "~/components/code/CodeBlock";
14+
import { BetaBadge } from "~/components/FeatureBadges";
15+
import { PageBody } from "~/components/layout/AppLayout";
16+
import { Button } from "~/components/primitives/Buttons";
17+
import { Header3 } from "~/components/primitives/Headers";
18+
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
19+
import { Paragraph } from "~/components/primitives/Paragraph";
20+
import { type EndpointsListRow } from "~/components/webhookEndpoints/v1/EndpointsListTable";
21+
import { WebhooksBetaPreview } from "~/components/webhookEndpoints/WebhooksBetaPreview";
22+
import type { EndpointActivity } from "~/presenters/v3/WebhookEndpointsListPresenter.server";
23+
24+
const taskExample = `import { webhook, webhooks } from "@trigger.dev/sdk";
25+
26+
export const stripe = webhooks.endpoint.define({
27+
id: "stripe",
28+
source: webhooks.stripe(),
29+
});
30+
31+
export const fulfillOrder = webhook({
32+
id: "fulfill-order",
33+
endpoint: stripe,
34+
filter: "event.type == 'checkout.session.completed'",
35+
onEvent: async ({ event }) => {
36+
await fulfill(event.data.object.id);
37+
},
38+
});`;
39+
40+
const agentExample = `import { webhooks } from "@trigger.dev/sdk";
41+
import { chat } from "@trigger.dev/sdk/ai";
42+
import { slack, webhookSource } from "@trigger.dev/slack";
43+
44+
const slackEvents = webhooks.endpoint.define({
45+
id: "slack",
46+
source: webhookSource(),
47+
});
48+
49+
export const support = chat.agent({
50+
id: "support",
51+
channels: [
52+
slack({
53+
id: "slack",
54+
endpoint: slackEvents,
55+
token: process.env.SLACK_BOT_TOKEN!,
56+
}),
57+
],
58+
run: async ({ messages, signal, streamText }) =>
59+
streamText({ model, messages, abortSignal: signal }),
60+
});`;
61+
62+
export function WebhooksBetaAccess({
63+
requested,
64+
previewEndpoints,
65+
previewActivity,
66+
}: {
67+
requested: boolean;
68+
previewEndpoints: EndpointsListRow[];
69+
previewActivity: Promise<EndpointActivity>;
70+
}) {
71+
const fetcher = useFetcher<{ requested: boolean }>();
72+
const isRequested = requested || fetcher.data?.requested === true;
73+
74+
return (
75+
<>
76+
<NavBar>
77+
<PageTitle title="Webhooks" accessory={<BetaBadge />} />
78+
</NavBar>
79+
<PageBody>
80+
<div className="mx-auto flex max-w-6xl flex-col gap-5 px-2 py-5">
81+
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-grid-bright bg-background-bright py-3 pl-4 pr-3">
82+
<Paragraph variant="base/bright">
83+
{isRequested
84+
? "Webhooks are in private beta. You're on the wait list for this organization."
85+
: "Webhooks are in private beta. Request access and we'll turn them on for your organization."}
86+
</Paragraph>
87+
<fetcher.Form method="post">
88+
{isRequested ? (
89+
<Button
90+
type="button"
91+
variant="secondary/medium"
92+
LeadingIcon={CheckIcon}
93+
leadingIconClassName="text-success"
94+
disabled
95+
>
96+
Access requested
97+
</Button>
98+
) : (
99+
<Button type="submit" variant="primary/medium" disabled={fetcher.state !== "idle"}>
100+
Request beta access
101+
</Button>
102+
)}
103+
</fetcher.Form>
104+
</div>
105+
106+
<WebhooksBetaPreview endpoints={previewEndpoints} activity={previewActivity} />
107+
108+
<div className="grid grid-cols-1 gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
109+
<FeatureGrid>
110+
<Feature icon={ShieldCheckIcon} title="Signature verification">
111+
Built-in sources for Stripe, GitHub, Slack, Linear and dozens more, or any HMAC
112+
scheme. Bad signatures are rejected before a run starts.
113+
</Feature>
114+
<Feature icon={ClockIcon} title="Delivery history and replay">
115+
Every delivery is stored with its headers and body. Replay any of them from the
116+
dashboard or the API.
117+
</Feature>
118+
<Feature icon={CodeBracketIcon} title="Typesafe filters">
119+
Filters are type checked against the provider's events, and only matching deliveries
120+
start a run.
121+
</Feature>
122+
<Feature icon={WebhookIcon} title="No server to run">
123+
Each endpoint gets its own URL hosted by Trigger.dev, so there's nothing for you to
124+
deploy or keep running.
125+
</Feature>
126+
</FeatureGrid>
127+
<CodeBlock code={taskExample} language="typescript" fileName="trigger/orders.ts" />
128+
</div>
129+
130+
<div className="grid grid-cols-1 gap-5 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)]">
131+
<CodeBlock code={agentExample} language="typescript" fileName="trigger/support.ts" />
132+
<FeatureGrid>
133+
<Feature icon={ChatBubbleLeftRightIcon} title="Events become messages">
134+
Route webhook events into a <code>chat.agent</code> conversation, so the agent
135+
replies with full context.
136+
</Feature>
137+
<Feature icon={PauseCircleIcon} title="Wait for a webhook">
138+
Pause a task until a matching webhook arrives, like a payment succeeding, then carry
139+
on from where it left off.
140+
</Feature>
141+
<Feature icon={BoltIcon} title="Fast acknowledgement">
142+
The endpoint responds right away, inside Slack's 3 second limit. The agent runs
143+
after that.
144+
</Feature>
145+
<Feature icon={ShieldCheckIcon} title="Slack built in">
146+
<code>@trigger.dev/slack</code> verifies Slack's signing secret for you, with no
147+
handler code.
148+
</Feature>
149+
</FeatureGrid>
150+
</div>
151+
</div>
152+
</PageBody>
153+
</>
154+
);
155+
}
156+
157+
function FeatureGrid({ children }: { children: ReactNode }) {
158+
return (
159+
<section className="grid grid-cols-1 overflow-hidden rounded-md border border-grid-bright bg-background-bright sm:grid-cols-2 [&>*]:border-grid-bright max-sm:[&>*:not(:last-child)]:border-b sm:[&>*:nth-child(-n+2)]:border-b sm:[&>*:nth-child(odd)]:border-r">
160+
{children}
161+
</section>
162+
);
163+
}
164+
165+
function Feature({
166+
icon: Icon,
167+
title,
168+
children,
169+
}: {
170+
icon: ComponentType<{ className?: string }>;
171+
title: string;
172+
children: ReactNode;
173+
}) {
174+
return (
175+
<div className="flex flex-col gap-2 p-4">
176+
<div className="grid size-7 place-items-center rounded border border-grid-bright bg-background-raised">
177+
<Icon className="size-4 text-webhooks" />
178+
</div>
179+
<Header3 className="mt-1">{title}</Header3>
180+
<Paragraph
181+
variant="small"
182+
className="[&_code]:font-mono [&_code]:text-xs [&_code]:text-text-bright"
183+
>
184+
{children}
185+
</Paragraph>
186+
</div>
187+
);
188+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { ArrowRightIcon, EyeIcon } from "@heroicons/react/20/solid";
2+
import type { WebhookDeliveryStatus } from "@trigger.dev/database";
3+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
4+
import { useEffect, useState } from "react";
5+
import { DeliveryStatusBadge } from "~/components/webhookDeliveries/v1/DeliveryStatus";
6+
import {
7+
type EndpointsListRow,
8+
EndpointsListTable,
9+
} from "~/components/webhookEndpoints/v1/EndpointsListTable";
10+
import type { EndpointActivity } from "~/presenters/v3/WebhookEndpointsListPresenter.server";
11+
12+
type SampleDelivery = {
13+
source: string;
14+
event: string;
15+
task?: string;
16+
status: WebhookDeliveryStatus;
17+
};
18+
19+
const SAMPLE_DELIVERIES: SampleDelivery[] = [
20+
{
21+
source: "stripe",
22+
event: "checkout.session.completed",
23+
task: "fulfill-order",
24+
status: "SUCCEEDED",
25+
},
26+
{ source: "slack", event: "app_mention", task: "support", status: "SUCCEEDED" },
27+
{ source: "github", event: "pull_request.opened", task: "review-pr", status: "SUCCEEDED" },
28+
{ source: "stripe", event: "invoice.paid", status: "FILTERED" },
29+
{ source: "linear", event: "Issue.create", task: "triage", status: "SUCCEEDED" },
30+
{ source: "shopify", event: "orders/create", task: "sync-order", status: "PROCESSING" },
31+
{ source: "github", event: "push", status: "FILTERED" },
32+
{ source: "clerk", event: "user.created", task: "onboard-user", status: "SUCCEEDED" },
33+
];
34+
35+
const VISIBLE_DELIVERIES = 3;
36+
const DELIVERY_INTERVAL_MS = 2200;
37+
38+
export function WebhooksBetaPreview({
39+
endpoints,
40+
activity,
41+
}: {
42+
endpoints: EndpointsListRow[];
43+
activity: Promise<EndpointActivity>;
44+
}) {
45+
return (
46+
<section
47+
aria-label="Preview of the Webhooks page with sample data"
48+
className="relative h-[22rem] overflow-hidden rounded-md border border-grid-bright bg-background-bright"
49+
>
50+
<div
51+
aria-hidden
52+
{...{ inert: "" }}
53+
className="pointer-events-none select-none opacity-70 saturate-[0.8]"
54+
>
55+
<EndpointsListTable endpoints={endpoints} activity={activity} hasFilters={false} />
56+
</div>
57+
58+
<div className="pointer-events-none absolute inset-0 backdrop-blur-[1px] [mask-image:linear-gradient(to_bottom,transparent_15%,black_40%)]" />
59+
<div className="pointer-events-none absolute inset-0 backdrop-blur-[3px] [mask-image:linear-gradient(to_bottom,transparent_40%,black_65%)]" />
60+
<div className="pointer-events-none absolute inset-0 backdrop-blur-[8px] [mask-image:linear-gradient(to_bottom,transparent_60%,black_85%)]" />
61+
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-transparent from-40% to-background-bright" />
62+
63+
<span className="absolute bottom-3 right-3 flex items-center gap-1.5 rounded-full border border-grid-bright bg-background-raised px-2.5 py-1 text-xs text-text-dimmed">
64+
<EyeIcon className="size-3.5" />
65+
Preview with sample data
66+
</span>
67+
68+
<DeliveryFeed />
69+
</section>
70+
);
71+
}
72+
73+
function DeliveryFeed() {
74+
const reduceMotion = useReducedMotion();
75+
const [count, setCount] = useState(VISIBLE_DELIVERIES);
76+
77+
useEffect(() => {
78+
if (reduceMotion) return;
79+
const interval = setInterval(() => setCount((current) => current + 1), DELIVERY_INTERVAL_MS);
80+
return () => clearInterval(interval);
81+
}, [reduceMotion]);
82+
83+
const visible = Array.from({ length: VISIBLE_DELIVERIES }, (_, offset) => count - 1 - offset);
84+
85+
return (
86+
<div aria-hidden className="absolute inset-x-0 bottom-12 flex flex-col items-center gap-2 px-4">
87+
<AnimatePresence initial={false} mode="popLayout">
88+
{visible.map((index, position) => (
89+
<motion.div
90+
key={index}
91+
layout
92+
initial={{ opacity: 0, y: -16, scale: 0.96 }}
93+
animate={{ opacity: 1 - position * 0.3, y: 0, scale: 1 - position * 0.03 }}
94+
exit={{ opacity: 0, y: 12, scale: 0.94 }}
95+
transition={{ type: "spring", stiffness: 380, damping: 32 }}
96+
className="w-full max-w-xl"
97+
>
98+
<DeliveryCard
99+
delivery={SAMPLE_DELIVERIES[index % SAMPLE_DELIVERIES.length]}
100+
isNewest={position === 0}
101+
/>
102+
</motion.div>
103+
))}
104+
</AnimatePresence>
105+
</div>
106+
);
107+
}
108+
109+
function DeliveryCard({ delivery, isNewest }: { delivery: SampleDelivery; isNewest: boolean }) {
110+
return (
111+
<div className="flex items-center gap-3 rounded-md border border-grid-bright bg-background-raised/95 px-3 py-2 text-sm shadow-lg">
112+
<span className="rounded border border-grid-bright px-1.5 py-0.5 font-mono text-xs text-text-bright">
113+
{delivery.source}
114+
</span>
115+
<span className="min-w-0 flex-1 truncate font-mono text-xs text-text-dimmed">
116+
{delivery.event}
117+
</span>
118+
{delivery.task ? (
119+
<span className="flex shrink-0 items-center gap-1.5 text-xs text-text-bright">
120+
<ArrowRightIcon className="size-3.5 text-text-dimmed" />
121+
<span className="font-mono">{delivery.task}</span>
122+
</span>
123+
) : null}
124+
<DeliveryStatusBadge status={delivery.status} className="w-24 shrink-0 text-xs" />
125+
<span className="w-8 shrink-0 text-right text-xs text-text-dimmed">
126+
{isNewest ? "now" : ""}
127+
</span>
128+
</div>
129+
);
130+
}

0 commit comments

Comments
 (0)