mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
improvement[webhook]: refactored webhook modal into components
This commit is contained in:
60
package-lock.json
generated
60
package-lock.json
generated
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "sim",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.5.0.tgz",
|
||||
"integrity": "sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.5.0",
|
||||
"motion-utils": "^12.5.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.5.0.tgz",
|
||||
"integrity": "sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.5.0.tgz",
|
||||
"integrity": "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.5.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { CopyableField } from '../ui/copyable'
|
||||
import { TestResultDisplay } from '../ui/test-result'
|
||||
|
||||
interface GenericConfigProps {
|
||||
requireAuth: boolean
|
||||
setRequireAuth: (requireAuth: boolean) => void
|
||||
generalToken: string
|
||||
setGeneralToken: (token: string) => void
|
||||
secretHeaderName: string
|
||||
setSecretHeaderName: (headerName: string) => void
|
||||
allowedIps: string
|
||||
setAllowedIps: (ips: string) => void
|
||||
isLoadingToken: boolean
|
||||
testResult: {
|
||||
success: boolean
|
||||
message?: string
|
||||
test?: any
|
||||
} | null
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
testWebhook: () => Promise<void>
|
||||
}
|
||||
|
||||
export function GenericConfig({
|
||||
requireAuth,
|
||||
setRequireAuth,
|
||||
generalToken,
|
||||
setGeneralToken,
|
||||
secretHeaderName,
|
||||
setSecretHeaderName,
|
||||
allowedIps,
|
||||
setAllowedIps,
|
||||
isLoadingToken,
|
||||
testResult,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
}: GenericConfigProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<div className="flex items-center h-3.5 space-x-2">
|
||||
<Checkbox
|
||||
id="require-auth"
|
||||
checked={requireAuth}
|
||||
onCheckedChange={(checked) => setRequireAuth(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="require-auth" className="text-sm font-medium cursor-pointer">
|
||||
Require Authentication
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{requireAuth && (
|
||||
<div className="space-y-4 ml-5 border-l-2 pl-4 border-gray-200 dark:border-gray-700">
|
||||
<CopyableField
|
||||
id="auth-token"
|
||||
label="Authentication Token"
|
||||
value={generalToken}
|
||||
onChange={setGeneralToken}
|
||||
placeholder="Enter an auth token"
|
||||
description="This token will be used to authenticate requests to your webhook (via Bearer token)."
|
||||
isLoading={isLoadingToken}
|
||||
copied={copied}
|
||||
copyType="general-token"
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="header-name">Secret Header Name (Optional)</Label>
|
||||
<Input
|
||||
id="header-name"
|
||||
value={secretHeaderName}
|
||||
onChange={(e) => setSecretHeaderName(e.target.value)}
|
||||
placeholder="X-Secret-Key"
|
||||
className="flex-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Custom HTTP header name for passing the authentication token instead of using Bearer
|
||||
authentication.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="allowed-ips">Allowed IP Addresses (Optional)</Label>
|
||||
<Input
|
||||
id="allowed-ips"
|
||||
value={allowedIps}
|
||||
onChange={(e) => setAllowedIps(e.target.value)}
|
||||
placeholder="192.168.1.1, 10.0.0.1"
|
||||
className="flex-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Comma-separated list of IP addresses that are allowed to access this webhook.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TestResultDisplay
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
showCurlCommand={true}
|
||||
/>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="font-medium text-sm mb-2">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Copy the Webhook URL above</li>
|
||||
<li>Configure your service to send HTTP POST requests to this URL</li>
|
||||
{requireAuth && (
|
||||
<>
|
||||
<li>
|
||||
{secretHeaderName
|
||||
? `Add the "${secretHeaderName}" header with your token to all requests`
|
||||
: 'Add an "Authorization: Bearer YOUR_TOKEN" header to all requests'}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div className="mt-4 pt-3 border-t 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>
|
||||
The webhook will receive all HTTP POST requests and pass the data to your workflow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface GithubConfigProps {
|
||||
contentType: string
|
||||
setContentType: (contentType: string) => void
|
||||
isLoadingToken: boolean
|
||||
testResult: {
|
||||
success: boolean
|
||||
message?: string
|
||||
test?: any
|
||||
} | null
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
}
|
||||
|
||||
export function GithubConfig({
|
||||
contentType,
|
||||
setContentType,
|
||||
isLoadingToken,
|
||||
testResult,
|
||||
}: GithubConfigProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="github-content-type">Content Type</Label>
|
||||
{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={contentType}
|
||||
onChange={(e) => setContentType(e.target.value)}
|
||||
placeholder="application/json"
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Go to your GitHub repository</li>
|
||||
<li>Navigate to Settings {'>'} Webhooks</li>
|
||||
<li>Click "Add webhook"</li>
|
||||
<li>Enter the Webhook URL shown above</li>
|
||||
<li>Set Content type to "{contentType}"</li>
|
||||
<li>Choose which events you want to trigger the webhook</li>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
interface StripeConfigProps {
|
||||
isLoadingToken: boolean
|
||||
testResult: {
|
||||
success: boolean
|
||||
message?: string
|
||||
test?: any
|
||||
} | null
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
}
|
||||
|
||||
export function StripeConfig({
|
||||
isLoadingToken,
|
||||
testResult,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
}: StripeConfigProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Go to your Stripe Dashboard</li>
|
||||
<li>Navigate to Developers {'>'} Webhooks</li>
|
||||
<li>Click "Add endpoint"</li>
|
||||
<li>Enter the Webhook URL shown above</li>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { CopyableField } from '../ui/copyable'
|
||||
import { TestResultDisplay } from '../ui/test-result'
|
||||
|
||||
interface WhatsAppConfigProps {
|
||||
verificationToken: string
|
||||
setVerificationToken: (token: string) => void
|
||||
isLoadingToken: boolean
|
||||
testResult: {
|
||||
success: boolean
|
||||
message?: string
|
||||
test?: any
|
||||
} | null
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
}
|
||||
|
||||
export function WhatsAppConfig({
|
||||
verificationToken,
|
||||
setVerificationToken,
|
||||
isLoadingToken,
|
||||
testResult,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
}: WhatsAppConfigProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<CopyableField
|
||||
id="whatsapp-verification-token"
|
||||
label="Verification Token"
|
||||
value={verificationToken}
|
||||
onChange={setVerificationToken}
|
||||
placeholder="Enter a verification token for WhatsApp"
|
||||
description="This token will be used to verify your webhook with WhatsApp."
|
||||
isLoading={isLoadingToken}
|
||||
copied={copied}
|
||||
copyType="token"
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
|
||||
<TestResultDisplay
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Setup Instructions</h4>
|
||||
<ol className="space-y-2">
|
||||
<li className="flex items-start">
|
||||
<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 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 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 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 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 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 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 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 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 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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
interface DeleteConfirmDialogProps {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
onConfirm: () => void
|
||||
isDeleting: boolean
|
||||
}
|
||||
|
||||
export function DeleteConfirmDialog({
|
||||
open,
|
||||
setOpen,
|
||||
onConfirm,
|
||||
isDeleting,
|
||||
}: DeleteConfirmDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will delete the webhook configuration. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700">
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
interface UnsavedChangesDialogProps {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export function UnsavedChangesDialog({
|
||||
open,
|
||||
setOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: UnsavedChangesDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Are you sure you want to close without saving?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700">
|
||||
Discard changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Check, Copy, Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface CopyableFieldProps {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
onChange?: (value: string) => void
|
||||
placeholder?: string
|
||||
description?: string
|
||||
isLoading?: boolean
|
||||
copied: string | null
|
||||
copyType: string
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
export function CopyableField({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
description,
|
||||
isLoading = false,
|
||||
copied,
|
||||
copyType,
|
||||
copyToClipboard,
|
||||
readOnly = false,
|
||||
}: CopyableFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
{isLoading ? (
|
||||
<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={id}
|
||||
value={value}
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined}
|
||||
placeholder={placeholder}
|
||||
className="flex-1"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(value, copyType)}
|
||||
disabled={isLoading || !value}
|
||||
>
|
||||
{copied === copyType ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
interface TestResultDisplayProps {
|
||||
testResult: {
|
||||
success: boolean
|
||||
message?: string
|
||||
test?: {
|
||||
curlCommand?: string
|
||||
status?: number
|
||||
contentType?: string
|
||||
responseText?: string
|
||||
headers?: Record<string, string>
|
||||
samplePayload?: Record<string, any>
|
||||
}
|
||||
} | null
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
showCurlCommand?: boolean
|
||||
}
|
||||
|
||||
export function TestResultDisplay({
|
||||
testResult,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
showCurlCommand = false,
|
||||
}: TestResultDisplayProps) {
|
||||
if (!testResult) return null
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
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>
|
||||
|
||||
{showCurlCommand && testResult.success && testResult.test?.curlCommand && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
className="mt-3 bg-black/10 dark:bg-white/10 p-2 rounded text-xs font-mono overflow-x-auto relative group"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-2 h-6 w-6 opacity-70 hover:opacity-100"
|
||||
onClick={() => copyToClipboard(testResult.test?.curlCommand || '', 'curl-command')}
|
||||
>
|
||||
{copied === 'curl-command' ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<pre className="whitespace-pre-wrap break-all pr-8">{testResult.test.curlCommand}</pre>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Loader2, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DialogFooter } from '@/components/ui/dialog'
|
||||
|
||||
interface WebhookDialogFooterProps {
|
||||
webhookId: string | undefined
|
||||
webhookProvider: string
|
||||
isSaving: boolean
|
||||
isDeleting: boolean
|
||||
isLoadingToken: boolean
|
||||
isTesting: boolean
|
||||
onSave: () => void
|
||||
onDelete: () => void
|
||||
onTest?: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function WebhookDialogFooter({
|
||||
webhookId,
|
||||
webhookProvider,
|
||||
isSaving,
|
||||
isDeleting,
|
||||
isLoadingToken,
|
||||
isTesting,
|
||||
onSave,
|
||||
onDelete,
|
||||
onTest,
|
||||
onClose,
|
||||
}: WebhookDialogFooterProps) {
|
||||
const showTestButton =
|
||||
webhookId && (webhookProvider === 'whatsapp' || webhookProvider === 'generic') && onTest
|
||||
|
||||
return (
|
||||
<DialogFooter className="flex justify-between mt-6 sticky bottom-0 py-3 bg-background border-t z-10">
|
||||
<div>
|
||||
{webhookId && (
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting || isLoadingToken}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{showTestButton && (
|
||||
<Button variant="outline" onClick={onTest} disabled={isTesting || isLoadingToken}>
|
||||
{isTesting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Testing...
|
||||
</>
|
||||
) : (
|
||||
'Test'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={onSave}
|
||||
disabled={isSaving || isLoadingToken}
|
||||
className="bg-primary"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { WEBHOOK_PROVIDERS } from '../../webhook-config'
|
||||
|
||||
interface WebhookDialogHeaderProps {
|
||||
webhookProvider: string
|
||||
webhookId: string | undefined
|
||||
}
|
||||
|
||||
export function WebhookDialogHeader({ webhookProvider, webhookId }: WebhookDialogHeaderProps) {
|
||||
const provider = WEBHOOK_PROVIDERS[webhookProvider] || WEBHOOK_PROVIDERS.generic
|
||||
|
||||
// Get provider icon
|
||||
const getProviderIcon = () => {
|
||||
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`
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
</DialogHeader>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Check, Copy, Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface WebhookUrlFieldProps {
|
||||
webhookUrl: string
|
||||
isLoadingToken: boolean
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
}
|
||||
|
||||
export function WebhookUrlField({
|
||||
webhookUrl,
|
||||
isLoadingToken,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
}: WebhookUrlFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="webhook-url">Webhook URL</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,16 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Check, Copy, Loader2, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { ProviderConfig, WEBHOOK_PROVIDERS } from '../webhook-config'
|
||||
import { GenericConfig } from './providers/generic-config'
|
||||
import { GithubConfig } from './providers/github-config'
|
||||
import { StripeConfig } from './providers/stripe-config'
|
||||
import { WhatsAppConfig } from './providers/whatsapp-config'
|
||||
import { DeleteConfirmDialog } from './ui/confirmation'
|
||||
import { UnsavedChangesDialog } from './ui/confirmation'
|
||||
import { WebhookDialogFooter } from './ui/webhook-footer'
|
||||
import { WebhookDialogHeader } from './ui/webhook-header'
|
||||
import { WebhookUrlField } from './ui/webhook-url'
|
||||
|
||||
const logger = createLogger('WebhookModal')
|
||||
|
||||
@@ -370,488 +355,58 @@ export function WebhookModal({
|
||||
}
|
||||
}
|
||||
|
||||
// Get provider icon
|
||||
const getProviderIcon = () => {
|
||||
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
|
||||
// Provider-specific component rendering
|
||||
const renderProviderContent = () => {
|
||||
switch (webhookProvider) {
|
||||
case 'whatsapp':
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="whatsapp-verification-token">Verification Token</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
{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" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This token will be used to verify your webhook with WhatsApp.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
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>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Setup Instructions</h4>
|
||||
<ol className="space-y-2">
|
||||
<li className="flex items-start">
|
||||
<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 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 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 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 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 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 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 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 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 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>
|
||||
</div>
|
||||
</div>
|
||||
<WhatsAppConfig
|
||||
verificationToken={whatsappVerificationToken}
|
||||
setVerificationToken={setWhatsappVerificationToken}
|
||||
isLoadingToken={isLoadingToken}
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
)
|
||||
case 'github':
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="github-content-type">Content Type</Label>
|
||||
{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">
|
||||
<h4 className="font-medium">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Go to your GitHub repository</li>
|
||||
<li>Navigate to Settings {'>'} Webhooks</li>
|
||||
<li>Click "Add webhook"</li>
|
||||
<li>Enter the Webhook URL shown above</li>
|
||||
<li>Set Content type to "{githubContentType}"</li>
|
||||
<li>Choose which events you want to trigger the webhook</li>
|
||||
<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>
|
||||
<GithubConfig
|
||||
contentType={githubContentType}
|
||||
setContentType={setGithubContentType}
|
||||
isLoadingToken={isLoadingToken}
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
)
|
||||
case 'stripe':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Go to your Stripe Dashboard</li>
|
||||
<li>Navigate to Developers {'>'} Webhooks</li>
|
||||
<li>Click "Add endpoint"</li>
|
||||
<li>Enter the Webhook URL shown above</li>
|
||||
<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>
|
||||
<StripeConfig
|
||||
isLoadingToken={isLoadingToken}
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
)
|
||||
case 'generic':
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<div className="flex items-center h-3.5 space-x-2">
|
||||
<Checkbox
|
||||
id="require-auth"
|
||||
checked={requireAuth}
|
||||
onCheckedChange={(checked) => setRequireAuth(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="require-auth" className="text-sm font-medium cursor-pointer">
|
||||
Require Authentication
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{requireAuth && (
|
||||
<div className="space-y-4 ml-5 border-l-2 pl-4 border-gray-200 dark:border-gray-700">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="auth-token">Authentication Token</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
{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="auth-token"
|
||||
value={generalToken}
|
||||
onChange={(e) => setGeneralToken(e.target.value)}
|
||||
placeholder="Enter an auth token"
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(generalToken, 'general-token')}
|
||||
disabled={isLoadingToken}
|
||||
>
|
||||
{copied === 'general-token' ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This token will be used to authenticate requests to your webhook (via Bearer
|
||||
token).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="header-name">Secret Header Name (Optional)</Label>
|
||||
<Input
|
||||
id="header-name"
|
||||
value={secretHeaderName}
|
||||
onChange={(e) => setSecretHeaderName(e.target.value)}
|
||||
placeholder="X-Secret-Key"
|
||||
className="flex-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Custom HTTP header name for passing the authentication token instead of using
|
||||
Bearer authentication.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="allowed-ips">Allowed IP Addresses (Optional)</Label>
|
||||
<Input
|
||||
id="allowed-ips"
|
||||
value={allowedIps}
|
||||
onChange={(e) => setAllowedIps(e.target.value)}
|
||||
placeholder="192.168.1.1, 10.0.0.1"
|
||||
className="flex-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Comma-separated list of IP addresses that are allowed to access this webhook.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{testResult && testResult.success && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="p-3 rounded-md bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 border border-green-200 dark:border-green-800"
|
||||
>
|
||||
<p className="text-sm">{testResult.message}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="font-medium text-sm mb-2">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Copy the Webhook URL above</li>
|
||||
<li>Configure your service to send HTTP POST requests to this URL</li>
|
||||
{requireAuth && (
|
||||
<>
|
||||
<li>
|
||||
{secretHeaderName
|
||||
? `Add the "${secretHeaderName}" header with your token to all requests`
|
||||
: 'Add an "Authorization: Bearer YOUR_TOKEN" header to all requests'}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div className="mt-4 pt-3 border-t 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>
|
||||
The webhook will receive all HTTP POST requests and pass the data to your
|
||||
workflow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testResult && testResult.success && testResult.test?.curlCommand && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
className="mt-3 bg-black/10 dark:bg-white/10 p-2 rounded text-xs font-mono overflow-x-auto relative group"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-2 h-6 w-6 opacity-70 hover:opacity-100"
|
||||
onClick={() =>
|
||||
copyToClipboard(testResult.test?.curlCommand || '', 'curl-command')
|
||||
}
|
||||
>
|
||||
{copied === 'curl-command' ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<pre className="whitespace-pre-wrap break-all pr-8">
|
||||
{testResult.test.curlCommand}
|
||||
</pre>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{testResult && !testResult.success && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="p-3 rounded-md 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>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="require-auth"
|
||||
checked={requireAuth}
|
||||
onCheckedChange={(checked) => setRequireAuth(checked === true)}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="require-auth" className="text-sm font-medium cursor-pointer">
|
||||
Require Authentication
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Enable authentication to secure your webhook endpoint.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{requireAuth && (
|
||||
<div className="space-y-4 pl-5 ml-1 border-l border-gray-200 dark:border-gray-700">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="auth-token">Authentication Token</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
{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="auth-token"
|
||||
value={generalToken}
|
||||
onChange={(e) => setGeneralToken(e.target.value)}
|
||||
placeholder="Enter an authentication token"
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(generalToken, 'general-token')}
|
||||
disabled={isLoadingToken}
|
||||
>
|
||||
{copied === 'general-token' ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This token will be used to authenticate requests to your webhook (via Bearer
|
||||
token).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="header-name">Secret Header Name (Optional)</Label>
|
||||
<Input
|
||||
id="header-name"
|
||||
value={secretHeaderName}
|
||||
onChange={(e) => setSecretHeaderName(e.target.value)}
|
||||
placeholder="X-Secret-Key"
|
||||
className="flex-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Custom HTTP header name for passing the authentication token instead of using
|
||||
Bearer authentication.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="allowed-ips">Allowed IP Addresses (Optional)</Label>
|
||||
<Input
|
||||
id="allowed-ips"
|
||||
value={allowedIps}
|
||||
onChange={(e) => setAllowedIps(e.target.value)}
|
||||
placeholder="192.168.1.1, 10.0.0.1"
|
||||
className="flex-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Comma-separated list of IP addresses that are allowed to access this webhook.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="font-medium text-sm mb-2">Setup Instructions</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>Copy the Webhook URL above</li>
|
||||
<li>Configure your service to send HTTP POST requests to this URL</li>
|
||||
{requireAuth && (
|
||||
<>
|
||||
<li>
|
||||
{secretHeaderName
|
||||
? `Add the "${secretHeaderName}" header with your token to all requests`
|
||||
: 'Add an "Authorization: Bearer YOUR_TOKEN" header to all requests'}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div className="mt-4 pt-3 border-t 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>
|
||||
The webhook will receive all HTTP POST requests and pass the data to your
|
||||
workflow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
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>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
<GenericConfig
|
||||
requireAuth={requireAuth}
|
||||
setRequireAuth={setRequireAuth}
|
||||
generalToken={generalToken}
|
||||
setGeneralToken={setGeneralToken}
|
||||
secretHeaderName={secretHeaderName}
|
||||
setSecretHeaderName={setSecretHeaderName}
|
||||
allowedIps={allowedIps}
|
||||
setAllowedIps={setAllowedIps}
|
||||
isLoadingToken={isLoadingToken}
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
testWebhook={testWebhook}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -860,153 +415,47 @@ export function WebhookModal({
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<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>
|
||||
</DialogHeader>
|
||||
<WebhookDialogHeader webhookProvider={webhookProvider} webhookId={webhookId} />
|
||||
|
||||
<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">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
<WebhookUrlField
|
||||
webhookUrl={webhookUrl}
|
||||
isLoadingToken={isLoadingToken}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
|
||||
{renderProviderContent()}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-between mt-6 sticky bottom-0 py-3 bg-background border-t z-10">
|
||||
<div>
|
||||
{webhookId && (
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isDeleting || isLoadingToken}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{(webhookProvider === 'whatsapp' || webhookProvider === 'generic') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={testWebhook}
|
||||
disabled={isTesting || isLoadingToken}
|
||||
>
|
||||
{isTesting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Testing...
|
||||
</>
|
||||
) : (
|
||||
'Test'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || isLoadingToken}
|
||||
className="bg-primary"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
<WebhookDialogFooter
|
||||
webhookId={webhookId}
|
||||
webhookProvider={webhookProvider}
|
||||
isSaving={isSaving}
|
||||
isDeleting={isDeleting}
|
||||
isLoadingToken={isLoadingToken}
|
||||
isTesting={isTesting}
|
||||
onSave={handleSave}
|
||||
onDelete={() => setShowDeleteConfirm(true)}
|
||||
onTest={testWebhook}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will delete the webhook configuration. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700">
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
setOpen={setShowDeleteConfirm}
|
||||
onConfirm={handleDelete}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
|
||||
<AlertDialog open={showUnsavedChangesConfirm} onOpenChange={setShowUnsavedChangesConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Are you sure you want to close without saving?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancelClose}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmClose} className="bg-red-600 hover:bg-red-700">
|
||||
Discard changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<UnsavedChangesDialog
|
||||
open={showUnsavedChangesConfirm}
|
||||
setOpen={setShowUnsavedChangesConfirm}
|
||||
onCancel={handleCancelClose}
|
||||
onConfirm={handleConfirmClose}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user