From df2a35ae5ce94f9b7f02bc78f32f8ff2f6adc29f Mon Sep 17 00:00:00 2001 From: Jason Endo Date: Fri, 5 Sep 2025 14:06:14 -0700 Subject: [PATCH 1/2] check if env var is set --- connect-react-demo/app/components/ClientWrapper.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/connect-react-demo/app/components/ClientWrapper.tsx b/connect-react-demo/app/components/ClientWrapper.tsx index 23e7981..ea32490 100644 --- a/connect-react-demo/app/components/ClientWrapper.tsx +++ b/connect-react-demo/app/components/ClientWrapper.tsx @@ -17,6 +17,7 @@ const ClientProviderWithLogger = () => { const client = externalUserId ? createLoggedFrontendClient( createFrontendClient({ + ...(process.env.NEXT_PUBLIC_PIPEDREAM_API_HOST && { apiHost: process.env.NEXT_PUBLIC_PIPEDREAM_API_HOST }), environment: process.env.PIPEDREAM_PROJECT_ENVIRONMENT, tokenCallback: fetchToken, externalUserId, From a4aaa4a655f168e4ef78d765f43add5cda451399 Mon Sep 17 00:00:00 2001 From: Jason Endo Date: Thu, 9 Oct 2025 16:48:34 -0700 Subject: [PATCH 2/2] add proxy support --- .../app/actions/backendClient.ts | 43 +++ .../app/components/ComponentTypeSelector.tsx | 14 +- .../app/components/ConfigPanel.tsx | 168 +++++++----- .../app/components/DemoPanel.tsx | 39 +-- .../app/components/ProxyRequestBuilder.tsx | 251 ++++++++++++++++++ .../app/components/config/AuthSection.tsx | 66 +++-- connect-react-demo/lib/app-state.tsx | 20 ++ 7 files changed, 499 insertions(+), 102 deletions(-) create mode 100644 connect-react-demo/app/components/ProxyRequestBuilder.tsx diff --git a/connect-react-demo/app/actions/backendClient.ts b/connect-react-demo/app/actions/backendClient.ts index 794169a..f107ec9 100644 --- a/connect-react-demo/app/actions/backendClient.ts +++ b/connect-react-demo/app/actions/backendClient.ts @@ -7,6 +7,14 @@ export type FetchTokenOpts = { externalUserId: string } +export type ProxyRequestOpts = { + externalUserId: string + accountId: string + url: string + method: string + data?: any +} + const allowedOrigins = ([ process.env.VERCEL_URL, process.env.VERCEL_BRANCH_URL, @@ -32,3 +40,38 @@ const _fetchToken = async (opts: FetchTokenOpts) => { // export const fetchToken = unstable_cache(_fetchToken, [], { revalidate: 3600 }) export const fetchToken = _fetchToken + +const _proxyRequest = async (opts: ProxyRequestOpts) => { + const serverClient = backendClient() + + try { + const proxyOptions = { + searchParams: { + external_user_id: opts.externalUserId, + account_id: opts.accountId + } + } + + const targetRequest = { + url: opts.url, + options: { + method: opts.method as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + ...(opts.data && { body: JSON.stringify(opts.data) }), + ...(opts.data && { headers: { "Content-Type": "application/json" } }) + } + } + + const resp = await serverClient.makeProxyRequest(proxyOptions, targetRequest); + return resp + } catch (error: any) { + // Re-throw with structured error info + throw { + message: error.message || 'Proxy request failed', + status: error.response?.status, + data: error.response?.data, + headers: error.response?.headers + } + } +} + +export const proxyRequest = _proxyRequest diff --git a/connect-react-demo/app/components/ComponentTypeSelector.tsx b/connect-react-demo/app/components/ComponentTypeSelector.tsx index 4930592..2af4929 100644 --- a/connect-react-demo/app/components/ComponentTypeSelector.tsx +++ b/connect-react-demo/app/components/ComponentTypeSelector.tsx @@ -1,10 +1,10 @@ import { cn } from "@/lib/utils" -import { IoCubeSharp, IoFlashOutline } from "react-icons/io5" +import { IoCubeSharp, IoFlashOutline, IoGlobe } from "react-icons/io5" import { TOGGLE_STYLES } from "@/lib/constants/ui" interface ComponentTypeSelectorProps { - selectedType: "action" | "trigger" - onTypeChange: (type: "action" | "trigger") => void + selectedType: "action" | "trigger" | "proxy" + onTypeChange: (type: "action" | "trigger" | "proxy") => void } const COMPONENT_TYPES = [ @@ -20,6 +20,12 @@ const COMPONENT_TYPES = [ icon: IoFlashOutline, description: "React to events and webhooks" }, + { + value: "proxy", + label: "Proxy", + icon: IoGlobe, + description: "Make direct API requests through authenticated accounts" + }, ] as const export function ComponentTypeSelector({ selectedType, onTypeChange }: ComponentTypeSelectorProps) { @@ -42,7 +48,7 @@ export function ComponentTypeSelector({ selectedType, onTypeChange }: ComponentT {type.label} - {index === 0 &&
} + {index < COMPONENT_TYPES.length - 1 &&
}
))}
diff --git a/connect-react-demo/app/components/ConfigPanel.tsx b/connect-react-demo/app/components/ConfigPanel.tsx index fc12b43..4d73f00 100644 --- a/connect-react-demo/app/components/ConfigPanel.tsx +++ b/connect-react-demo/app/components/ConfigPanel.tsx @@ -182,6 +182,10 @@ export const ConfigPanel = () => { propNames, webhookUrlValidationAttempted, setWebhookUrlValidationAttempted, + editableExternalUserId, + setEditableExternalUserId, + accountId, + setAccountId, } = useAppState() const id1 = useId(); const id2 = useId(); @@ -305,7 +309,7 @@ export const ConfigPanel = () => {
type{" "} componentType ={" "} - 'action' | 'trigger' + 'action' | 'trigger' | 'proxy'
@@ -334,48 +338,52 @@ export const ConfigPanel = () => { />
- - - { - app - ? setSelectedAppSlug(app.name_slug) - : removeSelectedAppSlug() - }} - /> - - - - - {selectedApp ? ( - { - comp - ? setSelectedComponentKey(comp.key) - : removeSelectedComponentKey() + {(selectedComponentType === "action" || selectedComponentType === "trigger") && ( + + + { + app + ? setSelectedAppSlug(app.name_slug) + : removeSelectedAppSlug() }} /> - ) : ( -
- Loading components... -
- )} -
-
+
+
+ )} + {selectedComponentType !== "proxy" && ( + + + {selectedApp ? ( + { + comp + ? setSelectedComponentKey(comp.key) + : removeSelectedComponentKey() + }} + /> + ) : ( +
+ Loading components... +
+ )} +
+
+ )} {selectedComponentType === "trigger" && ( { description="Authenticated user identifier" required={true} > - + {selectedComponentType === "proxy" ? ( + setEditableExternalUserId(e.target.value)} + placeholder="Enter external user ID" + className="w-full px-3 py-1.5 text-sm font-mono border rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" + /> + ) : ( + + )} + {selectedComponentType === "proxy" && ( + + setAccountId(e.target.value)} + placeholder="Enter account ID" + className="w-full px-3 py-1.5 text-sm font-mono border rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" + /> + + )} ) @@ -566,29 +598,33 @@ export const ConfigPanel = () => { {basicFormControls} {/* Desktop: Show with section header */} -
-
-

Additional Config Options

- {advancedFormControls} + {selectedComponentType !== "proxy" && ( +
+
+

Additional Config Options

+ {advancedFormControls} +
-
+ )} {/* Mobile: Collapsible */} -
- - -
- - More options -
- -
- - - {advancedFormControls} - -
-
+ {selectedComponentType !== "proxy" && ( +
+ + +
+ + More options +
+ +
+ + + {advancedFormControls} + +
+
+ )} {triggerInfo}
diff --git a/connect-react-demo/app/components/DemoPanel.tsx b/connect-react-demo/app/components/DemoPanel.tsx index 85d78d5..263edc0 100644 --- a/connect-react-demo/app/components/DemoPanel.tsx +++ b/connect-react-demo/app/components/DemoPanel.tsx @@ -4,6 +4,7 @@ import type { ConfigurableProps } from "@pipedream/sdk" import { useAppState } from "@/lib/app-state" import { PageSkeleton } from "./PageSkeleton" import { TerminalCollapsible } from "./TerminalCollapsible" +import { ProxyRequestBuilder } from "./ProxyRequestBuilder" export const DemoPanel = () => { const frontendClient = useFrontendClient() @@ -176,23 +177,27 @@ export const DemoPanel = () => { >
- - {selectedComponentKey && ( - - )} - + {selectedComponentType === "proxy" ? ( + + ) : ( + + {selectedComponentKey && ( + + )} + + )}
diff --git a/connect-react-demo/app/components/ProxyRequestBuilder.tsx b/connect-react-demo/app/components/ProxyRequestBuilder.tsx new file mode 100644 index 0000000..38ce305 --- /dev/null +++ b/connect-react-demo/app/components/ProxyRequestBuilder.tsx @@ -0,0 +1,251 @@ +import { useState } from "react" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Label } from "@/components/ui/label" +import { useAppState } from "@/lib/app-state" +import { proxyRequest } from "@/app/actions/backendClient" + +const HTTP_METHODS = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS" +] as const + +export function ProxyRequestBuilder() { + const { + proxyUrl, + setProxyUrl, + proxyMethod, + setProxyMethod, + proxyBody, + setProxyBody, + editableExternalUserId, + accountId, + selectedApp + } = useAppState() + + const [isLoading, setIsLoading] = useState(false) + const [response, setResponse] = useState(null) + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!proxyUrl.trim()) { + setError("URL is required") + return + } + + if (!editableExternalUserId?.trim()) { + setError("External User ID is required") + return + } + + if (!accountId?.trim()) { + setError("Account ID is required") + return + } + + + setIsLoading(true) + setError(null) + setResponse(null) + + try { + // Parse body if it's provided for POST/PUT/PATCH requests + let parsedBody: any = undefined + if (proxyBody.trim() && ["POST", "PUT", "PATCH"].includes(proxyMethod)) { + try { + parsedBody = JSON.parse(proxyBody) + } catch (parseError) { + setError("Invalid JSON in request body") + setIsLoading(false) + return + } + } + + // Make the actual proxy request using server action + const proxyResponse = await proxyRequest({ + externalUserId: editableExternalUserId, + accountId: accountId, + url: proxyUrl, + method: proxyMethod, + ...(parsedBody && { data: parsedBody }) + }) + + setResponse({ + status: 200, // Pipedream proxy returns 200 on success + data: proxyResponse, // The entire response is the data + headers: {}, // Headers might not be included in the response + request: { + url: proxyUrl, + method: proxyMethod, + body: parsedBody, + externalUserId: editableExternalUserId, + accountId + } + }) + } catch (err: any) { + setError(err?.message || "Request failed") + + // If there's response data in the error, show it + if (err?.status || err?.data) { + setResponse({ + status: err.status || 500, + error: true, + data: err.data, + headers: err.headers, + request: { + url: proxyUrl, + method: proxyMethod, + body: parsedBody, + externalUserId: editableExternalUserId, + accountId + } + }) + } + } finally { + setIsLoading(false) + } + } + + const showBodyField = ["POST", "PUT", "PATCH"].includes(proxyMethod) + + return ( +
+
+
+

API Request Builder

+

+ Make direct API requests through your authenticated account. +

+
+ +
+
+ + setProxyUrl(e.target.value)} + placeholder="https://api.example.com/endpoint%20or%20/api/v1/users" + className="font-mono text-sm" + /> +

+ Enter a full URL or a path (e.g., /api/v1/users) +

+
+ +
+ + +
+ + {showBodyField && ( +
+ +