Skip to content

Commit 40f5370

Browse files
committed
Add webhooks feature to Insight dashboard
1 parent 1e82083 commit 40f5370

File tree

17 files changed

+2677
-78
lines changed

17 files changed

+2677
-78
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"use server";
2+
3+
import { THIRDWEB_INSIGHT_API_DOMAIN } from "constants/urls";
4+
5+
export interface WebhookResponse {
6+
id: string;
7+
name: string;
8+
team_id: string;
9+
project_id: string;
10+
webhook_url: string;
11+
webhook_secret: string;
12+
filters: WebhookFilters;
13+
suspended_at: string | null;
14+
suspended_reason: string | null;
15+
disabled: boolean;
16+
created_at: string;
17+
updated_at: string | null;
18+
}
19+
20+
export interface WebhookFilters {
21+
"v1.events"?: {
22+
chain_ids?: string[];
23+
addresses?: string[];
24+
signatures?: Array<{
25+
sig_hash: string;
26+
abi?: string;
27+
params?: Record<string, unknown>;
28+
}>;
29+
};
30+
"v1.transactions"?: {
31+
chain_ids?: string[];
32+
from_addresses?: string[];
33+
to_addresses?: string[];
34+
signatures?: Array<{
35+
sig_hash: string;
36+
abi?: string;
37+
params?: string[];
38+
}>;
39+
};
40+
}
41+
42+
interface CreateWebhookPayload {
43+
webhook_url: string;
44+
filters: WebhookFilters;
45+
}
46+
47+
interface WebhooksListResponse {
48+
data: WebhookResponse[];
49+
}
50+
51+
interface WebhookSingleResponse {
52+
data: WebhookResponse;
53+
}
54+
55+
interface TestWebhookPayload {
56+
webhook_url: string;
57+
type?: "event" | "transaction";
58+
}
59+
60+
interface TestWebhookResponse {
61+
success: boolean;
62+
}
63+
64+
export async function createWebhook(
65+
payload: CreateWebhookPayload,
66+
clientId: string,
67+
): Promise<WebhookSingleResponse> {
68+
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
69+
method: "POST",
70+
headers: {
71+
"Content-Type": "application/json",
72+
"x-client-id": clientId,
73+
},
74+
body: JSON.stringify(payload),
75+
});
76+
77+
if (!response.ok) {
78+
const errorText = await response.text();
79+
throw new Error(`Failed to create webhook: ${errorText}`);
80+
}
81+
82+
return await response.json();
83+
}
84+
85+
export async function getWebhooks(
86+
clientId: string,
87+
): Promise<WebhooksListResponse> {
88+
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
89+
method: "GET",
90+
headers: {
91+
"x-client-id": clientId,
92+
},
93+
});
94+
95+
if (!response.ok) {
96+
const errorText = await response.text();
97+
throw new Error(`Failed to get webhooks: ${errorText}`);
98+
}
99+
100+
return await response.json();
101+
}
102+
103+
export async function deleteWebhook(
104+
webhookId: string,
105+
clientId: string,
106+
): Promise<WebhookSingleResponse> {
107+
const response = await fetch(
108+
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${webhookId}`,
109+
{
110+
method: "DELETE",
111+
headers: {
112+
"x-client-id": clientId,
113+
},
114+
},
115+
);
116+
117+
if (!response.ok) {
118+
const errorText = await response.text();
119+
throw new Error(`Failed to delete webhook: ${errorText}`);
120+
}
121+
122+
return await response.json();
123+
}
124+
125+
export async function testWebhook(
126+
payload: TestWebhookPayload,
127+
clientId: string,
128+
): Promise<TestWebhookResponse> {
129+
const response = await fetch(
130+
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
131+
{
132+
method: "POST",
133+
headers: {
134+
"Content-Type": "application/json",
135+
"x-client-id": clientId,
136+
},
137+
body: JSON.stringify(payload),
138+
},
139+
);
140+
141+
if (!response.ok) {
142+
const errorText = await response.text();
143+
throw new Error(`Failed to test webhook: ${errorText}`);
144+
}
145+
146+
return await response.json();
147+
}

apps/dashboard/src/@/components/blocks/SidebarLayout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function RenderSidebarGroup(props: {
137137
}
138138

139139
if ("separator" in link) {
140-
return <SidebarSeparator className="my-1" />;
140+
return <SidebarSeparator className="my-1" key="separator" />;
141141
}
142142

143143
return (

apps/dashboard/src/@/components/blocks/multi-select.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
165165
{props.customTrigger || (
166166
<Button
167167
ref={ref}
168-
{...props}
168+
{...(() => {
169+
// Extract customTrigger from props to avoid passing it to the DOM
170+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
171+
const { customTrigger, ...restProps } = props;
172+
return restProps;
173+
})()}
169174
onClick={handleTogglePopover}
170175
className={cn(
171176
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"use client";
2+
3+
import { TabPathLinks } from "@/components/ui/tabs";
4+
import { FooterLinksSection } from "../components/footer/FooterLinksSection";
5+
6+
export function InsightPageLayout(props: {
7+
projectSlug: string;
8+
projectId: string;
9+
teamSlug: string;
10+
children: React.ReactNode;
11+
}) {
12+
const insightLayoutSlug = `/team/${props.teamSlug}/${props.projectSlug}/insight`;
13+
14+
return (
15+
<div className="flex grow flex-col">
16+
{/* header */}
17+
<div className="pt-4 lg:pt-6">
18+
<div className="container flex max-w-7xl flex-col gap-4">
19+
<div>
20+
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
21+
Insight
22+
</h1>
23+
<p className="text-muted-foreground text-sm">
24+
APIs to retrieve blockchain data from any EVM chain, enrich it
25+
with metadata, and transform it using custom logic
26+
</p>
27+
</div>
28+
</div>
29+
30+
<div className="h-4" />
31+
32+
{/* Nav */}
33+
<TabPathLinks
34+
scrollableClassName="container max-w-7xl"
35+
links={[
36+
{
37+
name: "Overview",
38+
path: `${insightLayoutSlug}`,
39+
exactMatch: true,
40+
},
41+
{
42+
name: "Webhooks",
43+
path: `${insightLayoutSlug}/webhooks`,
44+
},
45+
]}
46+
/>
47+
</div>
48+
49+
{/* content */}
50+
<div className="h-6" />
51+
<div className="container flex max-w-7xl grow flex-col gap-6">
52+
<div>{props.children}</div>
53+
</div>
54+
<div className="h-20" />
55+
56+
{/* footer */}
57+
<div className="border-border border-t">
58+
<div className="container max-w-7xl">
59+
<InsightFooter />
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
}
65+
66+
function InsightFooter() {
67+
return (
68+
<FooterLinksSection
69+
left={{
70+
title: "Documentation",
71+
links: [
72+
{
73+
label: "Overview",
74+
href: "https://portal.thirdweb.com/insight",
75+
},
76+
{
77+
label: "API Reference",
78+
href: "https://insight-api.thirdweb.com/reference",
79+
},
80+
],
81+
}}
82+
center={{
83+
title: "Tutorials",
84+
links: [
85+
{
86+
label:
87+
"Blockchain Data on Any EVM - Quick and Easy REST APIs for Onchain Data",
88+
href: "https://www.youtube.com/watch?v=U2aW7YIUJVw",
89+
},
90+
{
91+
label: "Build a Whale Alerts Telegram Bot with Insight",
92+
href: "https://www.youtube.com/watch?v=HvqewXLVRig",
93+
},
94+
],
95+
}}
96+
right={{
97+
title: "Demos",
98+
links: [
99+
{
100+
label: "API Playground",
101+
href: "https://playground.thirdweb.com/insight",
102+
},
103+
],
104+
}}
105+
trackingCategory="insight"
106+
/>
107+
);
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getProject } from "@/api/projects";
2+
import { getTeamBySlug } from "@/api/team";
3+
import { redirect } from "next/navigation";
4+
import { InsightPageLayout } from "./InsightPageLayout";
5+
6+
export default async function Layout(props: {
7+
params: Promise<{ team_slug: string; project_slug: string }>;
8+
children: React.ReactNode;
9+
}) {
10+
const { team_slug, project_slug } = await props.params;
11+
12+
const [team, project] = await Promise.all([
13+
getTeamBySlug(team_slug),
14+
getProject(team_slug, project_slug),
15+
]);
16+
17+
if (!team) {
18+
redirect("/team");
19+
}
20+
21+
if (!project) {
22+
redirect(`/team/${team_slug}`);
23+
}
24+
25+
return (
26+
<InsightPageLayout
27+
projectSlug={project.slug}
28+
teamSlug={team_slug}
29+
projectId={project.id}
30+
>
31+
{props.children}
32+
</InsightPageLayout>
33+
);
34+
}

0 commit comments

Comments
 (0)