From c9431fad6f0ce730c80c9a164803c0244f5435a7 Mon Sep 17 00:00:00 2001 From: Prakash Maheshwaran <73785492+Prakashmaheshwaran@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:33:05 -0400 Subject: [PATCH] added UI for HTTP block (#2900) --- .../routes/workflows/editor/helpContent.ts | 20 +- .../HttpRequestNode/CurlImportDialog.tsx | 347 ++++++++++++++ .../nodes/HttpRequestNode/HttpRequestNode.tsx | 428 ++++++++++++++++++ .../nodes/HttpRequestNode/HttpUtils.tsx | 239 ++++++++++ .../HttpRequestNode/QuickHeadersDialog.tsx | 226 +++++++++ .../editor/nodes/HttpRequestNode/index.ts | 6 + .../editor/nodes/HttpRequestNode/types.ts | 34 ++ .../editor/nodes/WorkflowBlockIcon.tsx | 4 + .../routes/workflows/editor/nodes/index.ts | 6 +- .../routes/workflows/editor/nodes/types.ts | 1 + .../panels/WorkflowNodeLibraryPanel.tsx | 292 ++++++------ .../workflows/editor/workflowEditorUtils.ts | 62 ++- .../routes/workflows/types/workflowTypes.ts | 15 +- .../workflows/types/workflowYamlTypes.ts | 14 +- 14 files changed, 1553 insertions(+), 141 deletions(-) create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/CurlImportDialog.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/HttpRequestNode.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/HttpUtils.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/QuickHeadersDialog.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/index.ts create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/types.ts diff --git a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts index b99086e9..c17713ba 100644 --- a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts +++ b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts @@ -3,7 +3,7 @@ export const baseHelpTooltipContent = { navigationGoal: "Give Skyvern an objective. Make sure to include when the block is complete, when it should self-terminate, and any guardrails. Use {{ parameter_name }} to reference a parameter value", parameters: - "Define placeholder values using the “parameters” drop down that you predefine or redefine run-to-run.", + 'Define placeholder values using the "parameters" drop down that you predefine or redefine run-to-run.', dataExtractionGoal: "Tell Skyvern what data you would like to scrape at the end of your run.", dataSchema: "Specify a format for extracted data in JSON.", @@ -115,6 +115,17 @@ export const helpTooltips = { jsonSchema: "Specify a format for the extracted information from the file", }, url: baseHelpTooltipContent, + httpRequest: { + ...baseHelpTooltipContent, + url: "The URL to send the HTTP request to. You can use {{ parameter_name }} to reference parameters.", + method: "The HTTP method to use for the request.", + headers: "HTTP headers to include with the request as JSON object.", + body: "Request body as JSON object. Only used for POST, PUT, PATCH methods.", + timeout: "Request timeout in seconds.", + followRedirects: "Whether to automatically follow HTTP redirects.", + continueOnFailure: + "Allow the workflow to continue if the HTTP request fails.", + }, }; export const placeholders = { @@ -159,4 +170,11 @@ export const placeholders = { ...basePlaceholderContent, url: "(required) Navigate to this URL: https://...", }, + httpRequest: { + ...basePlaceholderContent, + url: "https://api.example.com/endpoint", + headers: + '{\n "Content-Type": "application/json",\n "Authorization": "Bearer {{ token }}"\n}', + body: '{\n "key": "value",\n "parameter": "{{ parameter_name }}"\n}', + }, }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/CurlImportDialog.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/CurlImportDialog.tsx new file mode 100644 index 00000000..e3569358 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/CurlImportDialog.tsx @@ -0,0 +1,347 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { useState } from "react"; +import { getClient } from "@/api/AxiosClient"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { toast } from "@/components/ui/use-toast"; +import { + ReloadIcon, + CodeIcon, + CheckIcon, + CopyIcon, +} from "@radix-ui/react-icons"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; + +type Props = { + onImport: (data: { + method: string; + url: string; + headers: string; + body: string; + timeout: number; + followRedirects: boolean; + }) => void; + children: React.ReactNode; +}; + +const curlExamples = [ + { + name: "GET Request", + curl: `curl -X GET "https://api.example.com/users" \\ + -H "Authorization: Bearer token123" \\ + -H "Accept: application/json"`, + }, + { + name: "POST JSON", + curl: `curl -X POST "https://api.example.com/users" \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer token123" \\ + -d '{"name": "John Doe", "email": "john@example.com"}'`, + }, + { + name: "PUT Request", + curl: `curl -X PUT "https://api.example.com/users/123" \\ + -H "Content-Type: application/json" \\ + -d '{"name": "Jane Doe"}'`, + }, +]; + +export function CurlImportDialog({ onImport, children }: Props) { + const [open, setOpen] = useState(false); + const [curlCommand, setCurlCommand] = useState(""); + const [loading, setLoading] = useState(false); + const [previewData, setPreviewData] = useState<{ + method: string; + url: string; + headers?: Record; + body?: unknown; + } | null>(null); + const credentialGetter = useCredentialGetter(); + + const handleImport = async () => { + if (!curlCommand.trim()) { + toast({ + title: "Error", + description: "Please enter a curl command", + variant: "destructive", + }); + return; + } + + setLoading(true); + try { + const client = await getClient(credentialGetter); + const response = await client.post("/utilities/curl-to-http", { + curl_command: curlCommand.trim(), + }); + + const data = response.data; + + onImport({ + method: data.method || "GET", + url: data.url || "", + headers: JSON.stringify(data.headers || {}, null, 2), + body: JSON.stringify(data.body || {}, null, 2), + timeout: data.timeout || 30, + followRedirects: data.follow_redirects ?? true, + }); + + toast({ + title: "Success", + description: "Curl command imported successfully", + variant: "success", + }); + + setOpen(false); + setCurlCommand(""); + setPreviewData(null); + } catch (error: unknown) { + const errorMessage = + ( + error as { + response?: { data?: { detail?: string } }; + message?: string; + } + ).response?.data?.detail || + (error as { message?: string }).message || + "Failed to parse curl command"; + toast({ + title: "Import Failed", + description: errorMessage, + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const handlePreview = async () => { + if (!curlCommand.trim()) return; + + setLoading(true); + try { + const client = await getClient(credentialGetter); + const response = await client.post("/utilities/curl-to-http", { + curl_command: curlCommand.trim(), + }); + setPreviewData(response.data); + } catch (error: unknown) { + const errorMessage = + ( + error as { + response?: { data?: { detail?: string } }; + message?: string; + } + ).response?.data?.detail || + (error as { message?: string }).message || + "Failed to parse curl command"; + toast({ + title: "Preview Failed", + description: errorMessage, + variant: "destructive", + }); + setPreviewData(null); + } finally { + setLoading(false); + } + }; + + const copyExample = (example: string) => { + navigator.clipboard.writeText(example); + toast({ + title: "Copied", + description: "Example copied to clipboard", + }); + }; + + return ( + + {children} + + + + + Import from cURL + + + Paste your curl command below and we'll automatically populate the + HTTP request fields. + + + +
+ {/* Left side - Input */} +
+
+ +