From eb2d49c4e64a0ab7a57e27eabb176bd99f923830 Mon Sep 17 00:00:00 2001 From: MananTank Date: Fri, 25 Jul 2025 19:20:22 +0000 Subject: [PATCH] Dashboard: Migrate engine/webhooks page from chakra to tailwind (#7717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on refactoring the webhook components in the dashboard application. It improves the code structure, updates UI components, and enhances the form handling for creating and managing webhooks. ### Detailed summary - Changed `CreateWebhookInput` type definition. - Refactored `EngineWebhooks` component to simplify props handling. - Updated UI components in `EngineWebhooks` for better styling. - Replaced Chakra UI modal with custom dialog components in `AddWebhookButton`. - Enhanced form validation using `zod` in `AddWebhookButton`. - Refactored `WebhooksTable` component for improved UI and functionality. - Replaced modals with dialogs for delete and test webhook actions. - Improved error handling and user notifications with `toast`. - Updated event type selection and input handling in forms. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **Refactor** * Updated webhooks-related components to use a custom UI library instead of Chakra UI, resulting in a more consistent interface. * Improved form validation and inline error messages when adding webhooks. * Enhanced dialog and modal interactions for adding, deleting, and testing webhooks with better state management. * Updated table and tooltip styling for better readability and user experience. * Adjusted button states and feedback to clearly indicate loading and error conditions. * Improved external links with enhanced security attributes and streamlined layout for webhook management. --- apps/dashboard/src/@/hooks/useEngine.ts | 2 +- .../components/add-webhook-button.tsx | 278 +++++++++------ .../webhooks/components/engine-webhooks.tsx | 54 ++- .../webhooks/components/webhooks-table.tsx | 336 ++++++++++-------- 4 files changed, 391 insertions(+), 279 deletions(-) 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 && } +
+ +
+ +
+
+ ); }