Skip to content

Commit efa083a

Browse files
author
Amine Afia
committed
Add webhooks feature to Insight dashboard
1 parent 1e82083 commit efa083a

File tree

10 files changed

+2636
-78
lines changed

10 files changed

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

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 6 additions & 1 deletion
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",
Lines changed: 108 additions & 0 deletions
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+
}
Lines changed: 34 additions & 0 deletions
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)