-
Notifications
You must be signed in to change notification settings - Fork 619
Add webhooks page to project dashboard #7172
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
...app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/CreateWebhookModal.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { Button } from "@/components/ui/button"; | ||
| // Implementation is going to be added in the next PR | ||
| export function CreateWebhookModal() { | ||
| return <Button>New Webhook</Button>; | ||
| } |
42 changes: 42 additions & 0 deletions
42
.../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/RelativeTime.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| "use client"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { formatDistanceToNowStrict } from "date-fns"; | ||
| import { useEffect, useState } from "react"; | ||
|
|
||
| export function RelativeTime({ | ||
| date, | ||
| className, | ||
| }: { date: string; className?: string }) { | ||
| const [content, setContent] = useState(() => { | ||
| const parsedDate = new Date(date); | ||
| if (Number.isNaN(parsedDate.getTime())) return "-"; | ||
| try { | ||
| return formatDistanceToNowStrict(parsedDate, { addSuffix: true }); | ||
| } catch { | ||
| return "-"; | ||
| } | ||
| }); | ||
|
|
||
| // eslint-disable-next-line | ||
| useEffect(() => { | ||
| const updateContent = () => { | ||
| const parsedDate = new Date(date); | ||
| if (Number.isNaN(parsedDate.getTime())) { | ||
| setContent("-"); | ||
| } else { | ||
| try { | ||
| setContent( | ||
| formatDistanceToNowStrict(parsedDate, { addSuffix: true }), | ||
| ); | ||
| } catch { | ||
| setContent("-"); | ||
| } | ||
| } | ||
| }; | ||
| updateContent(); | ||
| const interval = setInterval(updateContent, 10000); | ||
| return () => clearInterval(interval); | ||
| }, [date]); | ||
|
|
||
| return <span className={cn(className)}>{content}</span>; | ||
| } | ||
186 changes: 186 additions & 0 deletions
186
...app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/WebhooksTable.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| "use client"; | ||
|
|
||
| import { | ||
| type WebhookFilters, | ||
| type WebhookResponse, | ||
| deleteWebhook, | ||
| } from "@/api/insight/webhooks"; | ||
| import { CopyTextButton } from "@/components/ui/CopyTextButton"; | ||
| import { Spinner } from "@/components/ui/Spinner/Spinner"; | ||
| import { Badge } from "@/components/ui/badge"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { useDashboardRouter } from "@/lib/DashboardRouter"; | ||
| import type { ColumnDef } from "@tanstack/react-table"; | ||
| import { TWTable } from "components/shared/TWTable"; | ||
| import { format } from "date-fns"; | ||
| import { TrashIcon } from "lucide-react"; | ||
| import { useMemo, useState } from "react"; | ||
| import { toast } from "sonner"; | ||
| import { RelativeTime } from "./RelativeTime"; | ||
|
|
||
| function getEventType(filters: WebhookFilters): string { | ||
| if (filters["v1.events"]) return "Event"; | ||
| if (filters["v1.transactions"]) return "Transaction"; | ||
| return "Unknown"; | ||
| } | ||
|
|
||
| interface WebhooksTableProps { | ||
| webhooks: WebhookResponse[]; | ||
| clientId: string; | ||
| } | ||
|
|
||
| export function WebhooksTable({ webhooks, clientId }: WebhooksTableProps) { | ||
| const [isDeleting, setIsDeleting] = useState<Record<string, boolean>>({}); | ||
| // const { testWebhookEndpoint, isTestingMap } = useTestWebhook(clientId); | ||
| const router = useDashboardRouter(); | ||
|
|
||
| const handleDeleteWebhook = async (webhookId: string) => { | ||
| if (isDeleting[webhookId]) return; | ||
|
|
||
| try { | ||
| setIsDeleting((prev) => ({ ...prev, [webhookId]: true })); | ||
| await deleteWebhook(webhookId, clientId); | ||
| toast.success("Webhook deleted successfully"); | ||
| router.refresh(); | ||
| } catch (error) { | ||
| console.error("Error deleting webhook:", error); | ||
| toast.error("Failed to delete webhook", { | ||
| description: | ||
| error instanceof Error | ||
| ? error.message | ||
| : "An unexpected error occurred", | ||
| }); | ||
| } finally { | ||
| setIsDeleting((prev) => ({ ...prev, [webhookId]: false })); | ||
| } | ||
| }; | ||
|
|
||
| const columns: ColumnDef<WebhookResponse>[] = [ | ||
| { | ||
| accessorKey: "name", | ||
| header: "Name", | ||
| cell: ({ row }) => ( | ||
| <div className="flex items-center gap-2"> | ||
| <span className="max-w-40 truncate" title={row.original.name}> | ||
| {row.original.name} | ||
| </span> | ||
| </div> | ||
| ), | ||
| }, | ||
| { | ||
| accessorKey: "filters", | ||
| header: "Event Type", | ||
| cell: ({ getValue }) => { | ||
| const filters = getValue() as WebhookFilters; | ||
| if (!filters) return <span>-</span>; | ||
| const eventType = getEventType(filters); | ||
| return <span>{eventType}</span>; | ||
| }, | ||
| }, | ||
| { | ||
| accessorKey: "webhook_url", | ||
| header: "Webhook URL", | ||
| cell: ({ getValue }) => { | ||
| const url = getValue() as string; | ||
| return ( | ||
| <div className="flex items-center gap-2"> | ||
| <span className="max-w-60 truncate">{url}</span> | ||
| <CopyTextButton | ||
| textToCopy={url} | ||
| textToShow="" | ||
| tooltip="Copy URL" | ||
| variant="ghost" | ||
| copyIconPosition="right" | ||
| className="flex h-6 w-6 items-center justify-center" | ||
| iconClassName="h-3 w-3" | ||
| /> | ||
| </div> | ||
| ); | ||
| }, | ||
| }, | ||
| { | ||
| accessorKey: "created_at", | ||
| header: "Created", | ||
| cell: ({ getValue }) => { | ||
| const date = getValue() as string; | ||
| return ( | ||
| <div className="flex flex-col"> | ||
| <RelativeTime date={date} /> | ||
| <span className="text-muted-foreground text-xs"> | ||
| {format(new Date(date), "MMM d, yyyy")} | ||
AmineAfia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </span> | ||
| </div> | ||
| ); | ||
| }, | ||
| }, | ||
| { | ||
| id: "status", | ||
| accessorKey: "suspended_at", | ||
| header: "Status", | ||
| cell: ({ row }) => { | ||
| const webhook = row.original; | ||
| const isSuspended = Boolean(webhook.suspended_at); | ||
| return ( | ||
| <Badge variant={isSuspended ? "destructive" : "default"}> | ||
| {isSuspended ? "Suspended" : "Active"} | ||
| </Badge> | ||
| ); | ||
| }, | ||
| }, | ||
| { | ||
| id: "actions", | ||
| header: "Actions", | ||
| cell: ({ row }) => { | ||
| const webhook = row.original; | ||
|
|
||
| return ( | ||
| <div className="flex justify-end gap-2"> | ||
| <Button | ||
| size="icon" | ||
| variant="outline" | ||
| className="h-8 w-8 text-red-500 hover:border-red-700 hover:text-red-700" | ||
| onClick={() => handleDeleteWebhook(webhook.id)} | ||
| disabled={isDeleting[webhook.id]} | ||
| aria-label={`Delete webhook ${webhook.name}`} | ||
| > | ||
| {isDeleting[webhook.id] ? ( | ||
| <Spinner className="h-4 w-4" /> | ||
| ) : ( | ||
| <TrashIcon className="h-4 w-4" /> | ||
| )} | ||
| </Button> | ||
| </div> | ||
| ); | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| const sortedWebhooks = useMemo(() => { | ||
| return [...webhooks].sort((a, b) => { | ||
| const dateA = new Date(a.created_at); | ||
| const dateB = new Date(b.created_at); | ||
|
|
||
| // Handle invalid dates by treating them as epoch (0) | ||
| const timeA = Number.isNaN(dateA.getTime()) ? 0 : dateA.getTime(); | ||
| const timeB = Number.isNaN(dateB.getTime()) ? 0 : dateB.getTime(); | ||
|
|
||
| return timeB - timeA; | ||
| }); | ||
| }, [webhooks]); | ||
|
|
||
| return ( | ||
| <div className="w-full"> | ||
| <div className="mb-4 flex items-center justify-end"> | ||
| <Button type="button">New Webhook</Button> | ||
| </div> | ||
| <TWTable | ||
| data={sortedWebhooks} | ||
| columns={columns} | ||
| isPending={false} | ||
| isFetched={true} | ||
| title="Webhooks" | ||
| tableContainerClassName="mt-4" | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
91 changes: 86 additions & 5 deletions
91
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,89 @@ | ||
| // This is a temporary page to supress linting errors and will be implemented up in the stack | ||
| import { getWebhooks } from "@/api/insight/webhooks"; | ||
| import { type WebhookResponse, getWebhooks } from "@/api/insight/webhooks"; | ||
| import { getProject } from "@/api/projects"; | ||
| import { TrackedUnderlineLink } from "@/components/ui/tracked-link"; | ||
| import { notFound } from "next/navigation"; | ||
| import { CreateWebhookModal } from "./components/CreateWebhookModal"; | ||
| import { WebhooksTable } from "./components/WebhooksTable"; | ||
|
|
||
| export default async function WebhooksPage() { | ||
| const { data: webhooks, error } = await getWebhooks("123"); | ||
| export default async function WebhooksPage({ | ||
| params, | ||
| }: { params: Promise<{ team_slug: string; project_slug: string }> }) { | ||
| let webhooks: WebhookResponse[] = []; | ||
| let clientId = ""; | ||
| let errorMessage = ""; | ||
|
|
||
| return <div>{JSON.stringify({ webhooks, error })}</div>; | ||
| try { | ||
| // Await params before accessing properties | ||
| const resolvedParams = await params; | ||
| const team_slug = resolvedParams.team_slug; | ||
| const project_slug = resolvedParams.project_slug; | ||
|
|
||
| const project = await getProject(team_slug, project_slug); | ||
|
|
||
| if (!project) { | ||
| notFound(); | ||
| } | ||
|
|
||
| clientId = project.publishableKey; | ||
|
|
||
| const webhooksRes = await getWebhooks(clientId); | ||
| if (webhooksRes.error) { | ||
| errorMessage = webhooksRes.error; | ||
| } else if (webhooksRes.data) { | ||
| webhooks = webhooksRes.data; | ||
| } | ||
| } catch (error) { | ||
| errorMessage = "Failed to load webhooks. Please try again later."; | ||
| console.error("Error loading project or webhooks", error); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex grow flex-col"> | ||
| <div className="border-b py-10"> | ||
| <div className="container max-w-7xl"> | ||
| <h1 className="mb-1 font-semibold text-3xl tracking-tight"> | ||
| Webhooks | ||
| </h1> | ||
| <p className="text-muted-foreground text-sm"> | ||
| Create and manage webhooks to get notified about blockchain events, | ||
| transactions and more.{" "} | ||
| <TrackedUnderlineLink | ||
| category="webhooks" | ||
| label="learn-more" | ||
| target="_blank" | ||
| href="https://portal.thirdweb.com/insight/webhooks" | ||
| > | ||
| Learn more about webhooks. | ||
| </TrackedUnderlineLink> | ||
| </p> | ||
| </div> | ||
| </div> | ||
| <div className="h-6" /> | ||
| <div className="container max-w-7xl"> | ||
| {errorMessage ? ( | ||
| <div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-destructive bg-destructive/10 p-12 text-center"> | ||
| <div> | ||
| <h3 className="mb-1 font-medium text-destructive text-lg"> | ||
| Unable to load webhooks | ||
| </h3> | ||
| <p className="text-muted-foreground">{errorMessage}</p> | ||
| </div> | ||
| </div> | ||
| ) : webhooks.length > 0 ? ( | ||
| <WebhooksTable webhooks={webhooks} clientId={clientId} /> | ||
| ) : ( | ||
| <div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-border p-12 text-center"> | ||
| <div> | ||
| <h3 className="mb-1 font-medium text-lg">No webhooks found</h3> | ||
| <p className="text-muted-foreground"> | ||
| Create a webhook to get started. | ||
| </p> | ||
| </div> | ||
| <CreateWebhookModal /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| <div className="h-20" /> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.