From 3a7edbf1bf16df7489d0826987b784a1117eb7ad Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 17 Jul 2025 00:09:55 +0800 Subject: [PATCH] chore: accordion, rename, cleanup --- apps/dashboard/src/@/api/webhook-configs.ts | 50 + .../create-webhook-config-modal.tsx | 2 + .../components/edit-webhook-config-modal.tsx | 2 + .../components/topic-selector-modal.tsx | 1054 +++++++---------- .../components/webhook-config-modal.tsx | 182 ++- .../components/webhook-configs-table.tsx | 87 +- 6 files changed, 722 insertions(+), 655 deletions(-) diff --git a/apps/dashboard/src/@/api/webhook-configs.ts b/apps/dashboard/src/@/api/webhook-configs.ts index c830ecbc9db..c5b2b86403d 100644 --- a/apps/dashboard/src/@/api/webhook-configs.ts +++ b/apps/dashboard/src/@/api/webhook-configs.ts @@ -310,3 +310,53 @@ export async function deleteWebhookConfig(props: { status: "success", }; } + +type TestDestinationUrlResponse = + | { + result: { + httpStatusCode: number; + httpResponseBody: string; + }; + status: "success"; + } + | { + body: string; + reason: string; + status: "error"; + }; + +export async function testDestinationUrl(props: { + teamIdOrSlug: string; + projectIdOrSlug: string; + destinationUrl: string; +}): Promise { + const authToken = await getAuthToken(); + + if (!authToken) { + return { + body: "Authentication required", + reason: "no_auth_token", + status: "error", + }; + } + + const resp = await fetch( + `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/test-destination-url`, + { + body: JSON.stringify({ destinationUrl: props.destinationUrl }), + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + method: "POST", + }, + ); + if (!resp.ok) { + return { + body: await resp.text(), + reason: "unknown", + status: "error", + }; + } + return await resp.json(); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx index e226274a4ec..2417c4637ef 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx @@ -5,6 +5,7 @@ import { WebhookConfigModal } from "./webhook-config-modal"; interface CreateWebhookConfigModalProps { open: boolean; onOpenChange: (open: boolean) => void; + onSuccess: () => void; teamSlug: string; projectSlug: string; topics: Topic[]; @@ -19,6 +20,7 @@ export function CreateWebhookConfigModal(props: CreateWebhookConfigModalProps) { mode="create" onOpenChange={props.onOpenChange} open={props.open} + onSuccess={props.onSuccess} projectSlug={props.projectSlug} supportedChainIds={props.supportedChainIds} teamSlug={props.teamSlug} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx index 96932c1610c..60f2fcfd831 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx @@ -5,6 +5,7 @@ import { WebhookConfigModal } from "./webhook-config-modal"; interface EditWebhookConfigModalProps { open: boolean; onOpenChange: (open: boolean) => void; + onSuccess: () => void; teamSlug: string; projectSlug: string; topics: Topic[]; @@ -20,6 +21,7 @@ export function EditWebhookConfigModal(props: EditWebhookConfigModalProps) { mode="edit" onOpenChange={props.onOpenChange} open={props.open} + onSuccess={props.onSuccess} projectSlug={props.projectSlug} supportedChainIds={props.supportedChainIds} teamSlug={props.teamSlug} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/topic-selector-modal.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/topic-selector-modal.tsx index e7456529e55..defbaa4f090 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/topic-selector-modal.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/topic-selector-modal.tsx @@ -9,7 +9,12 @@ import { keccak256, toFunctionSelector } from "thirdweb/utils"; import type { Topic } from "@/api/webhook-configs"; import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors"; import { SignatureSelector } from "@/components/blocks/SignatureSelector"; -import { Badge } from "@/components/ui/badge"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -25,11 +30,10 @@ import { FormField, FormItem, FormLabel, - FormMessage, + RequiredFormLabel, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Textarea } from "@/components/ui/textarea"; import { useAbiMultiFetch } from "../hooks/useAbiProcessing"; import { extractEventSignatures, @@ -40,8 +44,8 @@ import type { WebhookFormValues } from "../utils/webhookTypes"; import { webhookFormSchema } from "../utils/webhookTypes"; const TOPIC_IDS_THAT_SUPPORT_FILTERS = [ - "insight.event.confirmed", - "insight.transaction.confirmed", + "contracts.event.confirmed", + "contracts.transaction.confirmed", ]; interface TopicSelectorModalProps { @@ -192,7 +196,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) { let formData: WebhookFormValues; // Get form data based on topic type - if (topic.id === "insight.event.confirmed") { + if (topic.id === "contracts.event.confirmed") { formData = eventFilterForm.getValues(); // Validate required fields for events @@ -229,7 +233,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) { } return { ...topic, filters }; - } else if (topic.id === "insight.transaction.confirmed") { + } else if (topic.id === "contracts.transaction.confirmed") { formData = transactionFilterForm.getValues(); // Validate required fields for transactions @@ -318,606 +322,470 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
-
- {Object.entries(groupedTopics).map(([service, topics]) => ( -
-

- {service} -

-
- {topics.map((topic) => ( -
- t.id === topic.id)} - id={topic.id} - onCheckedChange={(checked) => - handleTopicToggle(topic.id, !!checked) - } - /> -
- -

- {topic.description} -

- - {/* Show contract webhook filter form when selecting contract webhook topics */} - {TOPIC_IDS_THAT_SUPPORT_FILTERS.includes(topic.id) && - tempSelection.some((t) => t.id === topic.id) && - props.client && - props.supportedChainIds && ( -
-

- Configure{" "} - {topic.id === "insight.event.confirmed" - ? "Event" - : "Transaction"}{" "} - Filters -

- - {topic.id === "insight.event.confirmed" ? ( -
-
- {/* Chain IDs Field */} - ( - -
- - Chain IDs{" "} - - * - - -
- - {props.client ? ( - - field.onChange( - chainIds.map(String), - ) - } - selectedChainIds={ - Array.isArray(field.value) - ? field.value.map(Number) - : [] - } - /> - ) : ( -
- Client not available -
+
+ + {Object.entries(groupedTopics).map(([service, topics]) => { + const selectedCount = topics.filter((topic) => + tempSelection.some((selected) => selected.id === topic.id), + ).length; + const totalCount = topics.length; + + return ( + + +
+ + {service} + + + {selectedCount}/{totalCount} selected + +
+
+ +
+ {topics.map((topic) => ( +
+ t.id === topic.id, + )} + id={topic.id} + onCheckedChange={(checked) => + handleTopicToggle(topic.id, !!checked) + } + /> +
+ +

+ {topic.description} +

+ + {/* Show contract webhook filter form when selecting contract webhook topics */} + {TOPIC_IDS_THAT_SUPPORT_FILTERS.includes( + topic.id, + ) && + tempSelection.some((t) => t.id === topic.id) && + props.client && + props.supportedChainIds && ( +
+ {topic.id === + "contracts.event.confirmed" ? ( + +
+ {/* Chain IDs Field */} + ( + +
+ + Chain IDs + +
+ + {props.client && ( + + field.onChange( + chainIds.map(String), + ) + } + selectedChainIds={ + Array.isArray( + field.value, + ) + ? field.value.map( + Number, + ) + : [] + } + /> + )} + +
)} - - - - )} - /> - - {/* Contract Addresses for Events */} - ( - -
- - Contract Addresses{" "} - - * - - -
- -
- - - {/* ABI fetch status */} -
- {topic.id === - "insight.event.confirmed" && - eventAbi.isFetching && ( -
- - - Fetching ABIs... - + /> + + {/* Contract Addresses for Events */} + ( + +
+ + Contract Addresses + +
+ +
+ + + {/* ABI fetch status */} +
+ {topic.id === + "contracts.event.confirmed" && + eventAbi.isFetching && ( +
+ + + Fetching ABIs... + +
+ )}
- )} -
- - {/* ABI fetch results */} - {(Object.keys( - eventAbi.fetchedAbis, - ).length > 0 || - Object.keys(eventAbi.errors) - .length > 0) && ( -
- {Object.keys( +
+
+
+ )} + /> + + {/* Signature Hash Field */} + ( + +
+ + {topic.id === + "contracts.event.confirmed" + ? "Event Signature" + : "Function Signature"} + +
+ + {topic.id === + "contracts.event.confirmed" && + Object.keys( eventAbi.fetchedAbis, - ).length > 0 && ( -
- - ✓{" "} - { - Object.keys( - eventAbi.fetchedAbis, - ).length - }{" "} - ABI - {Object.keys( - eventAbi.fetchedAbis, - ).length !== 1 - ? "s" - : ""}{" "} - fetched - -
+ ).length > 0 && + eventAbi.signatures.length > + 0 ? ( + { + field.onChange(val); + // If custom signature, clear ABI field + const known = + eventAbi.signatures.map( + (sig) => + sig.signature, + ); + if ( + val && + !known.includes(val) + ) { + eventFilterForm.setValue( + "abi", + "", + ); + } + }} + options={eventAbi.signatures.map( + (sig) => ({ + abi: sig.abi, + label: truncateMiddle( + sig.name, + 30, + 15, + ), + value: sig.signature, + }), + )} + placeholder="Select or enter an event signature" + setAbi={(abi) => + eventFilterForm.setValue( + "sigHashAbi", + abi, + ) + } + value={field.value || ""} + /> + ) : topic.id === + "contracts.transaction.confirmed" && + Object.keys( + txAbi.fetchedAbis, + ).length > 0 && + txAbi.signatures.length > + 0 ? ( + { + field.onChange(val); + // If custom signature, clear ABI field + const known = + txAbi.signatures.map( + (sig) => + sig.signature, + ); + if ( + val && + !known.includes(val) + ) { + transactionFilterForm.setValue( + "abi", + "", + ); + } + }} + options={txAbi.signatures.map( + (sig) => ({ + abi: sig.abi, + label: truncateMiddle( + sig.name, + 30, + 15, + ), + value: sig.signature, + }), + )} + placeholder="Select or enter a function signature" + setAbi={(abi) => + transactionFilterForm.setValue( + "sigHashAbi", + abi, + ) + } + value={field.value || ""} + /> + ) : ( + )} - - {Object.keys(eventAbi.errors) - .length > 0 && ( -
- - ✗{" "} - { - Object.keys( - eventAbi.errors, - ).length - }{" "} - error - {Object.keys( - eventAbi.errors, - ).length !== 1 - ? "s" - : ""} - + + + )} + /> +
+ + ) : ( +
+
+ {/* Chain IDs Field */} + ( + +
+ + Chain IDs + +
+ + {props.client ? ( + + field.onChange( + chainIds.map(String), + ) + } + selectedChainIds={ + Array.isArray( + field.value, + ) + ? field.value.map( + Number, + ) + : [] + } + /> + ) : ( +
+ Client not available
)} -
- )} -
- - - - )} - /> - - {/* Signature Hash Field */} - ( - -
- - {topic.id === - "insight.event.confirmed" - ? "Event Signature (optional)" - : "Function Signature (optional)"} - -
- - {topic.id === - "insight.event.confirmed" && - Object.keys(eventAbi.fetchedAbis) - .length > 0 && - eventAbi.signatures.length > 0 ? ( - { - field.onChange(val); - // If custom signature, clear ABI field - const known = - eventAbi.signatures.map( - (sig) => sig.signature, - ); - if ( - val && - !known.includes(val) - ) { - eventFilterForm.setValue( - "abi", - "", - ); - } - }} - options={eventAbi.signatures.map( - (sig) => ({ - abi: sig.abi, - label: truncateMiddle( - sig.name, - 30, - 15, - ), - value: sig.signature, - }), - )} - placeholder="Select or enter an event signature" - setAbi={(abi) => - eventFilterForm.setValue( - "sigHashAbi", - abi, - ) - } - value={field.value || ""} - /> - ) : topic.id === - "insight.transaction.confirmed" && - Object.keys(txAbi.fetchedAbis) - .length > 0 && - txAbi.signatures.length > 0 ? ( - { - field.onChange(val); - // If custom signature, clear ABI field - const known = - txAbi.signatures.map( - (sig) => sig.signature, - ); - if ( - val && - !known.includes(val) - ) { - transactionFilterForm.setValue( - "abi", - "", - ); - } - }} - options={txAbi.signatures.map( - (sig) => ({ - abi: sig.abi, - label: truncateMiddle( - sig.name, - 30, - 15, - ), - value: sig.signature, - }), - )} - placeholder="Select or enter a function signature" - setAbi={(abi) => - transactionFilterForm.setValue( - "sigHashAbi", - abi, - ) - } - value={field.value || ""} - /> - ) : ( - + +
)} - - - - )} - /> -
- - ) : ( -
-
- {/* Chain IDs Field */} - ( - -
- - Chain IDs{" "} - - * - - -
- - {props.client ? ( - - field.onChange( - chainIds.map(String), - ) - } - selectedChainIds={ - Array.isArray(field.value) - ? field.value.map(Number) - : [] - } - /> - ) : ( -
- Client not available -
+ /> + + {/* From/To Addresses for Transactions */} + ( + +
+ + From Address + +
+ + + +
)} -
- -
- )} - /> - - {/* From/To Addresses for Transactions */} - ( - -
- - From Address{" "} - - * - - -
- - - - -
- )} - /> - - ( - -
- To Address -
- -
- - - {/* ABI fetch status */} -
- {txAbi.isFetching && ( -
- - - Fetching ABIs... - + /> + + ( + +
+ + To Address + +
+ +
+ + + {/* ABI fetch status */} +
+ {txAbi.isFetching && ( +
+ + + Fetching ABIs... + +
+ )} +
- )} -
- - {/* ABI fetch results */} - {(Object.keys(txAbi.fetchedAbis) - .length > 0 || - Object.keys(txAbi.errors) - .length > 0) && ( -
+ + + )} + /> + + {/* Signature Hash Field */} + ( + +
+ + Function Signature + +
+ {Object.keys( txAbi.fetchedAbis, - ).length > 0 && ( -
- - ✓{" "} - { - Object.keys( - txAbi.fetchedAbis, - ).length - }{" "} - ABI - {Object.keys( - txAbi.fetchedAbis, - ).length !== 1 - ? "s" - : ""}{" "} - fetched - -
+ ).length > 0 && + txAbi.signatures.length > + 0 ? ( + { + field.onChange(val); + // If custom signature, clear ABI field + const known = + txAbi.signatures.map( + (sig) => + sig.signature, + ); + if ( + val && + !known.includes(val) + ) { + transactionFilterForm.setValue( + "abi", + "", + ); + } + }} + options={txAbi.signatures.map( + (sig) => ({ + abi: sig.abi, + label: truncateMiddle( + sig.name, + 30, + 15, + ), + value: sig.signature, + }), + )} + placeholder="Select or enter a function signature" + setAbi={(abi) => + transactionFilterForm.setValue( + "sigHashAbi", + abi, + ) + } + value={field.value || ""} + /> + ) : ( + )} - - {Object.keys(txAbi.errors) - .length > 0 && ( -
- - ⚠️{" "} - { - Object.keys( - txAbi.errors, - ).length - }{" "} - error - {Object.keys( - txAbi.errors, - ).length !== 1 - ? "s" - : ""} - -
- )} -
- )} -
- - - - )} - /> - - {/* Signature Hash Field */} - ( - -
- - Function Signature (optional) - -
- - {Object.keys(txAbi.fetchedAbis) - .length > 0 && - txAbi.signatures.length > 0 ? ( - { - field.onChange(val); - // If custom signature, clear ABI field - const known = - txAbi.signatures.map( - (sig) => sig.signature, - ); - if ( - val && - !known.includes(val) - ) { - transactionFilterForm.setValue( - "abi", - "", - ); - } - }} - options={txAbi.signatures.map( - (sig) => ({ - abi: sig.abi, - label: truncateMiddle( - sig.name, - 30, - 15, - ), - value: sig.signature, - }), - )} - placeholder="Select or enter a function signature" - setAbi={(abi) => - transactionFilterForm.setValue( - "sigHashAbi", - abi, - ) - } - value={field.value || ""} - /> - ) : ( - + +
)} - - - - )} - /> + /> +
+ + )}
- - )} -
- )} - - {/* Show fallback for contract webhook topics when client/chain IDs not available */} - {TOPIC_IDS_THAT_SUPPORT_FILTERS.includes(topic.id) && - tempSelection.some((t) => t.id === topic.id) && - (!props.client || !props.supportedChainIds) && ( -
-