improvement: cleanup style and display for webhooks

This commit is contained in:
Waleed Latif
2025-03-08 20:16:05 -08:00
parent 6cbb4c0017
commit b53177cd14
2 changed files with 235 additions and 86 deletions
@@ -1,24 +1,85 @@
import { useEffect, useState } from 'react'
import { useParams } from 'next/navigation'
import { CheckCircle2, ExternalLink } from 'lucide-react'
import { GithubIcon, StripeIcon, WhatsAppIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import { WebhookModal } from '@/components/ui/webhook-modal'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
export interface WebhookProvider {
id: string
name: string
icon: (props: { className?: string }) => JSX.Element
configFields: {
[key: string]: {
type: 'string' | 'boolean' | 'select'
label: string
placeholder?: string
options?: string[]
defaultValue?: string | boolean
description?: string
}
}
}
// Define provider-specific configuration types
interface WhatsAppConfig {
export interface WhatsAppConfig {
verificationToken: string
}
interface GitHubConfig {
export interface GitHubConfig {
contentType: string
}
interface StripeConfig {
export interface StripeConfig {
// Any Stripe-specific fields would go here
}
type ProviderConfig = WhatsAppConfig | GitHubConfig | StripeConfig | Record<string, never>
// Union type for all provider configurations
export type ProviderConfig = WhatsAppConfig | GitHubConfig | StripeConfig | Record<string, never>
// Define available webhook providers
export const WEBHOOK_PROVIDERS: { [key: string]: WebhookProvider } = {
whatsapp: {
id: 'whatsapp',
name: 'WhatsApp',
icon: (props) => <WhatsAppIcon {...props} />,
configFields: {
verificationToken: {
type: 'string',
label: 'Verification Token',
placeholder: 'Enter a verification token for WhatsApp',
description: 'This token will be used to verify your webhook with WhatsApp.',
},
},
},
github: {
id: 'github',
name: 'GitHub',
icon: (props) => <GithubIcon {...props} />,
configFields: {
contentType: {
type: 'string',
label: 'Content Type',
placeholder: 'application/json',
defaultValue: 'application/json',
description: 'The content type for GitHub webhook payloads.',
},
},
},
stripe: {
id: 'stripe',
name: 'Stripe',
icon: (props) => <StripeIcon {...props} />,
configFields: {},
},
generic: {
id: 'generic',
name: 'Generic',
icon: (props) => <CheckCircle2 {...props} />,
configFields: {},
},
}
interface WebhookConfigProps {
blockId: string
@@ -36,7 +97,7 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
const workflowId = params.id as string
// Get the webhook provider from the block state
const [webhookProvider] = useSubBlockValue(blockId, 'webhookProvider')
const [webhookProvider, setWebhookProvider] = useSubBlockValue(blockId, 'webhookProvider')
// Store the webhook path
const [webhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath')
@@ -44,6 +105,9 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
// Store provider-specific configuration
const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig')
// Store the actual provider from the database
const [actualProvider, setActualProvider] = useState<string | null>(null)
// Check if webhook exists in the database
useEffect(() => {
const checkWebhook = async () => {
@@ -56,12 +120,21 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
const webhook = data.webhooks[0].webhook
setWebhookId(webhook.id)
// Update the provider in the block state if it's different
if (webhook.provider && webhook.provider !== webhookProvider) {
setWebhookProvider(webhook.provider)
}
// Store the actual provider from the database
setActualProvider(webhook.provider)
// Update the path in the block state if it's different
if (webhook.path && webhook.path !== webhookPath) {
setWebhookPath(webhook.path)
}
} else {
setWebhookId(null)
setActualProvider(null)
}
}
} catch (error) {
@@ -70,7 +143,7 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
}
checkWebhook()
}, [webhookPath, workflowId, setWebhookPath])
}, [webhookPath, webhookProvider, workflowId, setWebhookPath, setWebhookProvider])
const handleOpenModal = () => {
setIsModalOpen(true)
@@ -116,6 +189,9 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
const data = await response.json()
setWebhookId(data.webhook.id)
// Update the actual provider after saving
setActualProvider(webhookProvider || 'generic')
console.log('Webhook configuration saved successfully')
return true
} catch (error: any) {
@@ -143,8 +219,9 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
throw new Error(errorData.error || 'Failed to delete webhook')
}
// Clear the webhook ID
// Clear the webhook ID and actual provider
setWebhookId(null)
setActualProvider(null)
console.log('Webhook deleted successfully')
return true
@@ -157,9 +234,23 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
}
}
// Get provider icon based on the current provider
const getProviderIcon = () => {
// Only show provider icon if the webhook is connected and the selected provider matches the actual provider
if (!webhookId || webhookProvider !== actualProvider) {
return null
}
const provider = WEBHOOK_PROVIDERS[webhookProvider || 'generic']
return provider.icon({ className: 'h-4 w-4 mr-2 text-green-500 dark:text-green-400' })
}
// Check if the webhook is connected for the selected provider
const isWebhookConnected = webhookId && webhookProvider === actualProvider
return (
<div className="mt-2">
{error && <div className="text-sm text-red-500 mb-2">{error}</div>}
{error && <div className="text-sm text-red-500 dark:text-red-400 mb-2">{error}</div>}
<Button
variant="outline"
size="sm"
@@ -167,9 +258,9 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
onClick={handleOpenModal}
disabled={isConnecting || isSaving || isDeleting}
>
{webhookId ? (
{isWebhookConnected ? (
<>
<CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
{getProviderIcon()}
{isSaving ? 'Saving...' : isDeleting ? 'Deleting...' : 'Webhook Connected'}
</>
) : (
+134 -76
View File
@@ -23,21 +23,11 @@ import {
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
// Define provider-specific configuration types
interface WhatsAppConfig {
verificationToken: string
}
interface GitHubConfig {
contentType: string
}
interface StripeConfig {
// Any Stripe-specific fields would go here
}
type ProviderConfig = WhatsAppConfig | GitHubConfig | StripeConfig | Record<string, never>
import {
ProviderConfig,
WEBHOOK_PROVIDERS,
WebhookProvider,
} from '@/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config'
interface WebhookModalProps {
isOpen: boolean
@@ -64,6 +54,7 @@ export function WebhookModal({
const [isSaving, setIsSaving] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [isTesting, setIsTesting] = useState(false)
const [isLoadingToken, setIsLoadingToken] = useState(false)
const [testResult, setTestResult] = useState<{ success: boolean; message?: string } | null>(null)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const isConfigured = Boolean(webhookId)
@@ -72,13 +63,21 @@ export function WebhookModal({
const [whatsappVerificationToken, setWhatsappVerificationToken] = useState('')
const [githubContentType, setGithubContentType] = useState('application/json')
// Get the current provider configuration
const provider = WEBHOOK_PROVIDERS[webhookProvider] || WEBHOOK_PROVIDERS.generic
// Generate a random verification token if none exists
useEffect(() => {
if (webhookProvider === 'whatsapp' && !whatsappVerificationToken) {
if (
webhookProvider === 'whatsapp' &&
!whatsappVerificationToken &&
!webhookId &&
!isLoadingToken
) {
const randomToken = Math.random().toString(36).substring(2, 10)
setWhatsappVerificationToken(randomToken)
}
}, [webhookProvider, whatsappVerificationToken])
}, [webhookProvider, whatsappVerificationToken, webhookId, isLoadingToken])
// Load existing configuration values
useEffect(() => {
@@ -86,6 +85,7 @@ export function WebhookModal({
// If we have a webhook ID, try to fetch the existing configuration
const fetchWebhookConfig = async () => {
try {
setIsLoadingToken(true)
const response = await fetch(`/api/webhooks/${webhookId}`)
if (response.ok) {
const data = await response.json()
@@ -102,10 +102,16 @@ export function WebhookModal({
}
} catch (error) {
console.error('Error fetching webhook config:', error)
} finally {
setIsLoadingToken(false)
}
}
fetchWebhookConfig()
} else {
// If we don't have a webhook ID, we're creating a new one
// Reset the loading state
setIsLoadingToken(false)
}
}, [webhookId, webhookProvider])
@@ -208,7 +214,8 @@ export function WebhookModal({
if (data.success) {
setTestResult({
success: true,
message: data.message || 'Webhook configuration is valid.',
message:
data.message || 'Webhook configuration is valid. You can now use this URL in WhatsApp.',
})
} else {
setTestResult({
@@ -229,16 +236,12 @@ export function WebhookModal({
// Get provider icon
const getProviderIcon = () => {
switch (webhookProvider) {
case 'whatsapp':
return <WhatsAppIcon className="h-6 w-6 text-green-500" />
case 'github':
return <GithubIcon className="h-6 w-6" />
case 'stripe':
return <StripeIcon className="h-6 w-6 text-purple-500" />
default:
return null
}
return provider.icon({ className: 'h-5 w-5 text-green-500 dark:text-green-400' })
}
// Get provider-specific title
const getProviderTitle = () => {
return `${provider.name} Integration`
}
// Provider-specific setup instructions and configuration fields
@@ -250,18 +253,25 @@ export function WebhookModal({
<div className="space-y-2">
<Label htmlFor="whatsapp-verification-token">Verification Token</Label>
<div className="flex items-center space-x-2">
<Input
id="whatsapp-verification-token"
value={whatsappVerificationToken}
onChange={(e) => setWhatsappVerificationToken(e.target.value)}
placeholder="Enter a verification token for WhatsApp"
className="flex-1"
/>
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="whatsapp-verification-token"
value={whatsappVerificationToken}
onChange={(e) => setWhatsappVerificationToken(e.target.value)}
placeholder="Enter a verification token for WhatsApp"
className="flex-1"
/>
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(whatsappVerificationToken, 'token')}
disabled={isLoadingToken}
>
{copied === 'token' ? (
<Check className="h-4 w-4" />
@@ -277,7 +287,11 @@ export function WebhookModal({
{testResult && (
<div
className={`p-3 rounded-md ${testResult.success ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}
className={`p-3 rounded-md ${
testResult.success
? 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 border border-green-200 dark:border-green-800'
: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300 border border-red-200 dark:border-red-800'
}`}
>
<p className="text-sm">{testResult.message}</p>
</div>
@@ -287,56 +301,58 @@ export function WebhookModal({
<h4 className="font-medium">Setup Instructions</h4>
<ol className="space-y-2">
<li className="flex items-start">
<span className="text-gray-500 mr-2">1.</span>
<span className="text-gray-500 dark:text-gray-400 mr-2">1.</span>
<span className="text-sm">Go to WhatsApp Business Platform dashboard</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 mr-2">2.</span>
<span className="text-gray-500 dark:text-gray-400 mr-2">2.</span>
<span className="text-sm">Navigate to "Configuration" in the sidebar</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 mr-2">3.</span>
<span className="text-gray-500 dark:text-gray-400 mr-2">3.</span>
<span className="text-sm">
Enter the URL above as "Callback URL" (exactly as shown)
</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 mr-2">4.</span>
<span className="text-gray-500 dark:text-gray-400 mr-2">4.</span>
<span className="text-sm">Enter your token as "Verify token"</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 mr-2">5.</span>
<span className="text-gray-500 dark:text-gray-400 mr-2">5.</span>
<span className="text-sm">
Click "Verify and save" and subscribe to "messages"
</span>
</li>
</ol>
<div className="bg-blue-50 p-3 rounded-md mt-3">
<h5 className="text-sm font-medium text-blue-800">Requirements</h5>
<div className="bg-blue-50 dark:bg-blue-950 p-3 rounded-md mt-3 border border-blue-200 dark:border-blue-800">
<h5 className="text-sm font-medium text-blue-800 dark:text-blue-300">
Requirements
</h5>
<ul className="mt-1 space-y-1">
<li className="flex items-start">
<span className="text-blue-500 mr-2">•</span>
<span className="text-sm text-blue-700">
<span className="text-blue-500 dark:text-blue-400 mr-2">•</span>
<span className="text-sm text-blue-700 dark:text-blue-300">
URL must be publicly accessible with HTTPS
</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 mr-2">•</span>
<span className="text-sm text-blue-700">
<span className="text-blue-500 dark:text-blue-400 mr-2">•</span>
<span className="text-sm text-blue-700 dark:text-blue-300">
Self-signed SSL certificates not supported
</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 mr-2">•</span>
<span className="text-sm text-blue-700">
<span className="text-blue-500 dark:text-blue-400 mr-2">•</span>
<span className="text-sm text-blue-700 dark:text-blue-300">
For local testing, use ngrok to expose your server
</span>
</li>
</ul>
</div>
<div className="bg-gray-50 p-3 rounded-md mt-3">
<p className="text-sm text-gray-700 flex items-center">
<span className="text-gray-400 mr-2">💡</span>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
After saving, use "Test" to verify your webhook configuration.
</p>
</div>
@@ -348,13 +364,19 @@ export function WebhookModal({
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="github-content-type">Content Type</Label>
<Input
id="github-content-type"
value={githubContentType}
onChange={(e) => setGithubContentType(e.target.value)}
placeholder="application/json"
className="flex-1"
/>
{isLoadingToken ? (
<div className="h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="github-content-type"
value={githubContentType}
onChange={(e) => setGithubContentType(e.target.value)}
placeholder="application/json"
className="flex-1"
/>
)}
</div>
<div className="space-y-2">
@@ -369,6 +391,13 @@ export function WebhookModal({
<li>Ensure "Active" is checked and save</li>
</ol>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
After saving, GitHub will send a ping event to verify your webhook.
</p>
</div>
</div>
)
case 'stripe':
@@ -383,6 +412,13 @@ export function WebhookModal({
<li>Select the events you want to listen for</li>
<li>Add the endpoint</li>
</ol>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
Stripe will send a test event to verify your webhook endpoint.
</p>
</div>
</div>
)
default:
@@ -390,6 +426,13 @@ export function WebhookModal({
<div className="space-y-2">
<h4 className="font-medium">Generic Webhook Setup</h4>
<p className="text-sm">Use the URL above to send webhook events to this workflow.</p>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
You can test your webhook by sending a POST request to the URL.
</p>
</div>
</div>
)
}
@@ -399,32 +442,47 @@ export function WebhookModal({
<>
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader className="flex flex-row items-start">
<div className="mr-3">{getProviderIcon()}</div>
<div>
<DialogTitle>Webhook Configuration</DialogTitle>
<DialogDescription>Configure your WhatsApp integration</DialogDescription>
<DialogHeader className="flex flex-row items-center justify-between">
<div className="flex items-center">
<div className="mr-3 flex items-center">{getProviderIcon()}</div>
<div>
<DialogTitle>{getProviderTitle()}</DialogTitle>
<DialogDescription>
{webhookProvider === 'generic'
? 'Configure your webhook integration'
: `Configure your ${provider.name.toLowerCase()} integration`}
</DialogDescription>
</div>
</div>
<div className="flex items-center space-x-2">
{webhookId && (
<Badge
variant="outline"
className="bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800"
>
Connected
</Badge>
)}
</div>
{webhookId && (
<Badge
variant="outline"
className="ml-auto bg-green-50 text-green-700 border-green-200"
>
Connected
</Badge>
)}
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label htmlFor="webhook-url">Webhook URL</Label>
<div className="flex items-center space-x-2">
<Input id="webhook-url" value={webhookUrl} readOnly className="flex-1" />
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input id="webhook-url" value={webhookUrl} readOnly className="flex-1" />
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(webhookUrl, 'url')}
disabled={isLoadingToken}
>
{copied === 'url' ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
@@ -440,7 +498,7 @@ export function WebhookModal({
<Button
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={isDeleting}
disabled={isDeleting || isLoadingToken}
className="w-full sm:w-auto"
size="sm"
>
@@ -460,7 +518,7 @@ export function WebhookModal({
<Button
variant="outline"
onClick={testWebhook}
disabled={isTesting}
disabled={isTesting || isLoadingToken}
className="w-full sm:w-auto"
size="sm"
>
@@ -482,7 +540,7 @@ export function WebhookModal({
</Button>
<Button
onClick={handleSave}
disabled={isSaving}
disabled={isSaving || isLoadingToken}
className="w-full sm:w-auto"
size="sm"
>