import { NodeProps, NodeToolbar, Position } from "@xyflow/react"; import { AlertCircle, Circle, Edit, Link2, Loader2, PlusIcon, Trash2Icon } from "lucide-react"; import { memo, useCallback, useEffect, useState } from "react"; import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext"; import { createCredentialApiV1CredentialsPost, listCredentialsApiV1CredentialsGet, } from "@/client"; import { CredentialResponse, WebhookCredentialType } from "@/client/types.gen"; import { FlowNodeData } from "@/components/flow/types"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { JsonEditor, validateJson } from "@/components/ui/json-editor"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useAuth } from "@/lib/auth"; import { NodeContent } from "./common/NodeContent"; import { NodeEditDialog } from "./common/NodeEditDialog"; import { useNodeHandlers } from "./common/useNodeHandlers"; interface WebhookNodeProps extends NodeProps { data: FlowNodeData; } type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; interface CustomHeader { key: string; value: string; } export const WebhookNode = memo(({ data, selected, id }: WebhookNodeProps) => { const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({ id }); const { saveWorkflow } = useWorkflow(); const { getAccessToken } = useAuth(); // Form state const [name, setName] = useState(data.name || "Webhook"); const [enabled, setEnabled] = useState(data.enabled ?? true); const [httpMethod, setHttpMethod] = useState(data.http_method || "POST"); const [endpointUrl, setEndpointUrl] = useState(data.endpoint_url || ""); const [credentialUuid, setCredentialUuid] = useState(data.credential_uuid || ""); const [customHeaders, setCustomHeaders] = useState( data.custom_headers || [] ); const [payloadTemplate, setPayloadTemplate] = useState( data.payload_template ? JSON.stringify(data.payload_template, null, 2) : "{}" ); // Credentials state const [credentials, setCredentials] = useState([]); const [credentialsLoading, setCredentialsLoading] = useState(false); // Fetch credentials when dialog opens const fetchCredentials = useCallback(async () => { setCredentialsLoading(true); try { const accessToken = await getAccessToken(); const response = await listCredentialsApiV1CredentialsGet({ headers: { Authorization: `Bearer ${accessToken}` }, }); if (response.error) { console.error("Failed to fetch credentials:", response.error); setCredentials([]); return; } if (response.data) { setCredentials(response.data); } } catch (error) { console.error("Failed to fetch credentials:", error); setCredentials([]); } finally { setCredentialsLoading(false); } }, [getAccessToken]); // Validation state - only shown on save attempt const [jsonError, setJsonError] = useState(null); const [endpointError, setEndpointError] = useState(null); const handleSave = async () => { // Validate endpoint URL if (!endpointUrl.trim()) { setEndpointError('Endpoint URL is required'); return; } setEndpointError(null); // Validate JSON payload const validation = validateJson(payloadTemplate); if (!validation.valid) { setJsonError(validation.error || 'Invalid JSON. Please fix the payload template before saving.'); return; } setJsonError(null); handleSaveNodeData({ ...data, name, enabled, http_method: httpMethod, endpoint_url: endpointUrl, credential_uuid: credentialUuid || undefined, custom_headers: customHeaders.filter((h) => h.key && h.value), payload_template: validation.parsed as Record, }); setOpen(false); setTimeout(async () => { await saveWorkflow(); }, 100); }; const handleOpenChange = (newOpen: boolean) => { if (newOpen) { setName(data.name || "Webhook"); setEnabled(data.enabled ?? true); setHttpMethod(data.http_method || "POST"); setEndpointUrl(data.endpoint_url || ""); setCredentialUuid(data.credential_uuid || ""); setCustomHeaders(data.custom_headers || []); setPayloadTemplate( data.payload_template ? JSON.stringify(data.payload_template, null, 2) : "{}" ); // Clear any previous errors setJsonError(null); setEndpointError(null); // Fetch credentials when dialog opens fetchCredentials(); } setOpen(newOpen); }; useEffect(() => { if (open) { setName(data.name || "Webhook"); setEnabled(data.enabled ?? true); setHttpMethod(data.http_method || "POST"); setEndpointUrl(data.endpoint_url || ""); setCredentialUuid(data.credential_uuid || ""); setCustomHeaders(data.custom_headers || []); setPayloadTemplate( data.payload_template ? JSON.stringify(data.payload_template, null, 2) : "{}" ); } }, [data, open]); const truncateUrl = (url: string, maxLength: number = 30) => { if (!url) return "Not configured"; if (url.length <= maxLength) return url; return url.substring(0, maxLength) + "..."; }; return ( <> } nodeType="webhook" onDoubleClick={() => handleOpenChange(true)} nodeId={id} >
{data.http_method || "POST"} {truncateUrl(data.endpoint_url || "")}
{data.enabled !== false ? "Enabled" : "Disabled"}
{open && ( )} ); }); interface WebhookNodeEditFormProps { name: string; setName: (value: string) => void; enabled: boolean; setEnabled: (value: boolean) => void; httpMethod: HttpMethod; setHttpMethod: (value: HttpMethod) => void; endpointUrl: string; setEndpointUrl: (value: string) => void; credentialUuid: string; setCredentialUuid: (value: string) => void; credentials: CredentialResponse[]; credentialsLoading: boolean; onRefreshCredentials: () => Promise; getAccessToken: () => Promise; customHeaders: CustomHeader[]; setCustomHeaders: (value: CustomHeader[]) => void; payloadTemplate: string; setPayloadTemplate: (value: string) => void; } const WebhookNodeEditForm = ({ name, setName, enabled, setEnabled, httpMethod, setHttpMethod, endpointUrl, setEndpointUrl, credentialUuid, setCredentialUuid, credentials, credentialsLoading, onRefreshCredentials, getAccessToken, customHeaders, setCustomHeaders, payloadTemplate, setPayloadTemplate, }: WebhookNodeEditFormProps) => { // Add Credential Dialog state const [isAddCredentialOpen, setIsAddCredentialOpen] = useState(false); const [newCredName, setNewCredName] = useState(""); const [newCredDescription, setNewCredDescription] = useState(""); const [newCredType, setNewCredType] = useState("bearer_token"); const [newCredData, setNewCredData] = useState>({}); const [isCreatingCredential, setIsCreatingCredential] = useState(false); const [credentialError, setCredentialError] = useState(null); const handleCreateCredential = async () => { if (!newCredName.trim()) return; setIsCreatingCredential(true); setCredentialError(null); try { const accessToken = await getAccessToken(); const response = await createCredentialApiV1CredentialsPost({ headers: { Authorization: `Bearer ${accessToken}` }, body: { name: newCredName, description: newCredDescription || undefined, credential_type: newCredType, credential_data: newCredData, }, }); if (response.error) { const errorDetail = (response.error as { detail?: string })?.detail || "Failed to create credential"; setCredentialError(errorDetail); return; } if (response.data) { // Refresh credentials list await onRefreshCredentials(); // Select the newly created credential setCredentialUuid(response.data.uuid); // Close dialog and reset form setIsAddCredentialOpen(false); setNewCredName(""); setNewCredDescription(""); setNewCredType("bearer_token"); setNewCredData({}); setCredentialError(null); } } catch (error) { console.error("Failed to create credential:", error); setCredentialError( error instanceof Error ? error.message : "An unexpected error occurred" ); } finally { setIsCreatingCredential(false); } }; const handleAddCredentialDialogChange = (open: boolean) => { setIsAddCredentialOpen(open); if (!open) { // Reset error when closing dialog setCredentialError(null); } }; const getCredentialDataFields = (type: WebhookCredentialType) => { switch (type) { case "api_key": return [ { key: "header_name", label: "Header Name", placeholder: "X-API-Key" }, { key: "api_key", label: "API Key", placeholder: "your-api-key", isSecret: true }, ]; case "bearer_token": return [ { key: "token", label: "Token", placeholder: "your-bearer-token", isSecret: true }, ]; case "basic_auth": return [ { key: "username", label: "Username", placeholder: "username" }, { key: "password", label: "Password", placeholder: "password", isSecret: true }, ]; case "custom_header": return [ { key: "header_name", label: "Header Name", placeholder: "X-Custom-Header" }, { key: "header_value", label: "Header Value", placeholder: "header-value", isSecret: true }, ]; default: return []; } }; const addHeader = () => { setCustomHeaders([...customHeaders, { key: "", value: "" }]); }; const updateHeader = (index: number, field: "key" | "value", value: string) => { const newHeaders = [...customHeaders]; newHeaders[index] = { ...newHeaders[index], [field]: value }; setCustomHeaders(newHeaders); }; const removeHeader = (index: number) => { setCustomHeaders(customHeaders.filter((_, i) => i !== index)); }; const availableVariables = [ { name: "workflow_run_id", description: "Unique ID of the workflow run" }, { name: "workflow_id", description: "ID of the workflow" }, { name: "workflow_name", description: "Name of the workflow" }, { name: "initial_context.*", description: "Initial context variables" }, { name: "gathered_context.*", description: "Extracted variables" }, { name: "cost_info.call_duration_seconds", description: "Call duration" }, { name: "recording_url", description: "Call recording URL" }, { name: "transcript_url", description: "Transcript URL" }, ]; return ( Basic Auth Headers Payload
setName(e.target.value)} />
setEndpointUrl(e.target.value)} placeholder="https://api.example.com/webhook" />
{credentials.length === 0 && !credentialsLoading && (

No credentials found. Click the + button to create one.

)} {/* Add Credential Dialog */} Add Credential Create a new credential for webhook authentication. {/* Error display */} {credentialError && (
{credentialError}
)}
setNewCredName(e.target.value)} placeholder="My API Key" />
setNewCredDescription(e.target.value)} placeholder="Optional description" />
{getCredentialDataFields(newCredType).map((field) => (
setNewCredData((prev) => ({ ...prev, [field.key]: e.target.value, })) } placeholder={field.placeholder} />
))}
{customHeaders.map((header, index) => (
updateHeader(index, "key", e.target.value)} className="flex-1" /> updateHeader(index, "value", e.target.value)} className="flex-1" />
))}
{availableVariables.map((v) => (
{`{{${v.name}}}`} {v.description}
))}
); }; WebhookNode.displayName = "WebhookNode";