diff --git a/apps/dashboard/src/@/hooks/useEngine.ts b/apps/dashboard/src/@/hooks/useEngine.ts index b7ce2eb0bc8..8caa447ca8b 100644 --- a/apps/dashboard/src/@/hooks/useEngine.ts +++ b/apps/dashboard/src/@/hooks/useEngine.ts @@ -1223,7 +1223,7 @@ export function useEngineUpdateAccessToken(params: { }); } -export type CreateWebhookInput = { +type CreateWebhookInput = { url: string; name: string; eventType: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/add-webhook-button.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/add-webhook-button.tsx index 2f95e199259..ad32ef2f3e3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/add-webhook-button.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/add-webhook-button.tsx @@ -1,33 +1,38 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { - Flex, + Form, FormControl, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Select, - useDisclosure, -} from "@chakra-ui/react"; -import { Button } from "chakra/button"; -import { FormLabel } from "chakra/form"; -import { CirclePlusIcon } from "lucide-react"; -import { useForm } from "react-hook-form"; + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { - type CreateWebhookInput, - useEngineCreateWebhook, -} from "@/hooks/useEngine"; -import { useTxNotifications } from "@/hooks/useTxNotifications"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useEngineCreateWebhook } from "@/hooks/useEngine"; import { beautifyString } from "./webhooks-table"; -interface AddWebhookButtonProps { - instanceUrl: string; - authToken: string; -} - const WEBHOOK_EVENT_TYPES = [ "all_transactions", "sent_transaction", @@ -38,97 +43,156 @@ const WEBHOOK_EVENT_TYPES = [ "auth", ]; -export const AddWebhookButton: React.FC = ({ +const webhookFormSchema = z.object({ + eventType: z.string().min(1, "Event type is required"), + name: z.string().min(1, "Name is required"), + url: z.string().url("Please enter a valid URL"), +}); + +type WebhookFormValues = z.infer; + +export function AddWebhookButton({ instanceUrl, authToken, -}) => { - const { isOpen, onOpen, onClose } = useDisclosure(); - const { mutate: createWebhook } = useEngineCreateWebhook({ +}: { + instanceUrl: string; + authToken: string; +}) { + const [open, setOpen] = useState(false); + const createWebhook = useEngineCreateWebhook({ authToken, instanceUrl, }); - const form = useForm(); + const form = useForm({ + resolver: zodResolver(webhookFormSchema), + defaultValues: { + eventType: "", + name: "", + url: "", + }, + mode: "onChange", + }); - const { onSuccess, onError } = useTxNotifications( - "Webhook created successfully.", - "Failed to create webhook.", - ); + const onSubmit = (data: WebhookFormValues) => { + createWebhook.mutate(data, { + onError: (error) => { + toast.error("Failed to create webhook", { + description: error.message, + }); + console.error(error); + }, + onSuccess: () => { + toast.success("Webhook created successfully"); + setOpen(false); + form.reset(); + }, + }); + }; return ( - <> - + + + + - - - { - createWebhook(data, { - onError: (error) => { - onError(error); - console.error(error); - }, - onSuccess: () => { - onSuccess(); - onClose(); - }, - }); - })} - > - Create Webhook - - - - - Event Type - - - - Name - - - - URL - - - - + + + Create Webhook + + Create a new webhook to receive notifications for engine events. + + - - - - - - - +
+ +
+ ( + + Event Type + + + + )} + /> + + ( + + Name + + + + + + )} + /> + + ( + + URL + + + + + + )} + /> +
+ +
+ + +
+
+ + +
); -}; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/engine-webhooks.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/engine-webhooks.tsx index 65719b202d3..b905cac1829 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/engine-webhooks.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/engine-webhooks.tsx @@ -1,43 +1,38 @@ "use client"; -import { Heading } from "chakra/heading"; -import { Link } from "chakra/link"; -import { Text } from "chakra/text"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { useEngineWebhooks } from "@/hooks/useEngine"; import { AddWebhookButton } from "./add-webhook-button"; import { WebhooksTable } from "./webhooks-table"; -interface EngineWebhooksProps { - instanceUrl: string; - authToken: string; -} - -export const EngineWebhooks: React.FC = ({ +export function EngineWebhooks({ instanceUrl, authToken, -}) => { +}: { + instanceUrl: string; + authToken: string; +}) { const webhooks = useEngineWebhooks({ authToken, instanceUrl, }); return ( -
-
- Webhooks - - Notify your app backend when transaction and backend wallet events - occur.{" "} - - Learn more about webhooks - - . - -
+
+

Webhooks

+

+ Notify your app backend when transaction and backend wallet events + occur.{" "} + + Learn more about webhooks + + . +

+ = ({ isPending={webhooks.isPending} webhooks={webhooks.data || []} /> - + +
+ +
); -}; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/webhooks-table.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/webhooks-table.tsx index 94693a0bcee..e799982647f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/webhooks-table.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/webhooks/components/webhooks-table.tsx @@ -1,36 +1,30 @@ -import { - Flex, - FormControl, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Tooltip, - type UseDisclosureReturn, - useDisclosure, -} from "@chakra-ui/react"; import { createColumnHelper } from "@tanstack/react-table"; -import { Card } from "chakra/card"; -import { FormLabel } from "chakra/form"; -import { Text } from "chakra/text"; import { format, formatDistanceToNowStrict } from "date-fns"; -import { MailQuestionIcon, TrashIcon } from "lucide-react"; +import { ForwardIcon, RotateCcwIcon, TrashIcon } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { TWTable } from "@/components/blocks/TWTable"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; -import { FormItem } from "@/components/ui/form"; +import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code"; +import { DynamicHeight } from "@/components/ui/DynamicHeight"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ToolTipLabel } from "@/components/ui/tooltip"; import { type EngineWebhook, useEngineDeleteWebhook, useEngineTestWebhook, } from "@/hooks/useEngine"; +import { parseError } from "@/utils/errorParser"; import { shortenString } from "@/utils/usedapp-external"; export function beautifyString(str: string): string { @@ -40,26 +34,27 @@ export function beautifyString(str: string): string { .join(" "); } -interface WebhooksTableProps { - instanceUrl: string; - webhooks: EngineWebhook[]; - isPending: boolean; - isFetched: boolean; - authToken: string; -} - const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor("name", { cell: (cell) => { - return {cell.getValue()}; + return ( + {cell.getValue() || "N/A"} + ); }, header: "Name", }), columnHelper.accessor("eventType", { cell: (cell) => { - return {beautifyString(cell.getValue())}; + return ( + + {beautifyString(cell.getValue())} + + ); }, header: "Event Type", }), @@ -71,6 +66,8 @@ const columns = [ textToCopy={cell.getValue() || ""} textToShow={shortenString(cell.getValue() || "")} tooltip="Secret" + variant="ghost" + className="-translate-x-2 text-muted-foreground font-mono" /> ); }, @@ -80,9 +77,9 @@ const columns = [ cell: (cell) => { const url = cell.getValue(); return ( - + {url} - + ); }, header: "URL", @@ -96,35 +93,33 @@ const columns = [ const date = new Date(value); return ( - - {format(date, "PP pp z")} - - } - shouldWrapChildren - > - {formatDistanceToNowStrict(date, { addSuffix: true })} - + + + {formatDistanceToNowStrict(date, { addSuffix: true })} + + ); }, header: "Created At", }), ]; -export const WebhooksTable: React.FC = ({ +export function WebhooksTable({ instanceUrl, webhooks, isPending, isFetched, authToken, -}) => { +}: { + instanceUrl: string; + webhooks: EngineWebhook[]; + isPending: boolean; + isFetched: boolean; + authToken: string; +}) { const [selectedWebhook, setSelectedWebhook] = useState(); - const deleteDisclosure = useDisclosure(); - const testDisclosure = useDisclosure(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [testDialogOpen, setTestDialogOpen] = useState(false); const activeWebhooks = webhooks.filter((webhook) => webhook.active); @@ -137,10 +132,10 @@ export const WebhooksTable: React.FC = ({ isPending={isPending} onMenuClick={[ { - icon: , + icon: , onClick: (row) => { setSelectedWebhook(row); - testDisclosure.onOpen(); + setTestDialogOpen(true); }, text: "Test webhook", }, @@ -149,7 +144,7 @@ export const WebhooksTable: React.FC = ({ isDestructive: true, onClick: (row) => { setSelectedWebhook(row); - deleteDisclosure.onOpen(); + setDeleteDialogOpen(true); }, text: "Delete", }, @@ -157,114 +152,150 @@ export const WebhooksTable: React.FC = ({ title="webhooks" /> - {selectedWebhook && deleteDisclosure.isOpen && ( - )} - {selectedWebhook && testDisclosure.isOpen && ( - )} ); -}; - -interface DeleteWebhookModalProps { - webhook: EngineWebhook; - disclosure: UseDisclosureReturn; - instanceUrl: string; - authToken: string; } -function DeleteWebhookModal({ + +function DeleteWebhookDialog({ webhook, - disclosure, instanceUrl, authToken, -}: DeleteWebhookModalProps) { + open, + onOpenChange, +}: { + webhook: EngineWebhook; + instanceUrl: string; + authToken: string; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { const deleteWebhook = useEngineDeleteWebhook({ authToken, instanceUrl, }); - const onDelete = () => { - const promise = deleteWebhook.mutateAsync( + const onDelete = async () => { + await deleteWebhook.mutateAsync( { id: webhook.id }, { onError: (error) => { - console.error(error); + toast.error("Failed to delete webhook", { + description: parseError(error), + }); }, onSuccess: () => { - disclosure.onClose(); + onOpenChange(false); + toast.success("Webhook deleted successfully"); }, }, ); - - toast.promise(promise, { - error: "Failed to delete webhook.", - success: "Successfully deleted webhook.", - }); }; return ( - - - - Delete Webhook - - -
- Are you sure you want to delete this webhook? - - Name - {webhook.name} - - - URL - {webhook.url} - - - Created at - - {format(new Date(webhook.createdAt ?? ""), "PP pp z")} - - + + + + Delete Webhook + + Are you sure you want to delete this webhook? + + + +
+
+

Name

+ + {webhook.name || "N/A"} + +
+
+

URL

+

{webhook.url}

- +
+

Created at

+ + {format(new Date(webhook.createdAt ?? ""), "PP pp z")} + +
+
- - - - - - +
+ + ); } -interface TestWebhookModalProps { +function TestWebhookDialog({ + webhook, + instanceUrl, + authToken, + open, + onOpenChange, +}: { webhook: EngineWebhook; - disclosure: UseDisclosureReturn; instanceUrl: string; authToken: string; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + return ( + + + + Test Webhook + + + + + + ); } -function TestWebhookModal({ - webhook, - disclosure, - instanceUrl, - authToken, -}: TestWebhookModalProps) { - const { mutate: testWebhook, isPending } = useEngineTestWebhook({ + +function TestWebhookDialogContent(props: { + webhook: EngineWebhook; + instanceUrl: string; + authToken: string; +}) { + const { webhook, instanceUrl, authToken } = props; + + const testWebhook = useEngineTestWebhook({ authToken, instanceUrl, }); @@ -273,7 +304,7 @@ function TestWebhookModal({ const [body, setBody] = useState(); const onTest = () => { - testWebhook( + testWebhook.mutate( { id: webhook.id }, { onSuccess: (result) => { @@ -285,36 +316,55 @@ function TestWebhookModal({ }; return ( - - - - Test Webhook - - -
- - URL - {webhook.url} - - - + +
+
+
+

URL

+ {/* {webhook.url} */} + +
- {status && ( -
- - {status} - + {body && !testWebhook.isPending && ( +
+
+

+ Response +

+ {status && ( + + {status} + + )}
- )} -
- {body ?? "Send a request to see the response."} +
-
- - - + )} + + {testWebhook.isPending && } +
+ +
+ +
+
+ ); }