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.
+
+
+
+
+
+
+ {error && (
+
+ )}
+
+ {response && (
+
+
+
Response
+
= 200 && response.status < 300
+ ? 'bg-green-100 text-green-700'
+ : response.status >= 400
+ ? 'bg-red-100 text-red-700'
+ : 'bg-gray-100 text-gray-700'
+ }`}>
+ Status: {response.status}
+
+
+
+ {response.headers && (
+
+
Headers
+
+
+ {JSON.stringify(response.headers, null, 2)}
+
+
+
+ )}
+
+
+
Response Data
+
+
+ {JSON.stringify(response.data, null, 2)}
+
+
+
+
+
+
+ Request Details
+
+
+
+ {JSON.stringify(response.request, null, 2)}
+
+
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/connect-react-demo/app/components/config/AuthSection.tsx b/connect-react-demo/app/components/config/AuthSection.tsx
index 81f4868..9be02c7 100644
--- a/connect-react-demo/app/components/config/AuthSection.tsx
+++ b/connect-react-demo/app/components/config/AuthSection.tsx
@@ -4,27 +4,63 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
+import { Input } from "@/components/ui/input"
interface AuthSectionProps {
externalUserId: string
+ selectedComponentType: string
+ editableExternalUserId?: string
+ setEditableExternalUserId?: (value: string) => void
+ accountId?: string
+ setAccountId?: (value: string) => void
}
-export const AuthSection = ({ externalUserId }: AuthSectionProps) => (
+export const AuthSection = ({
+ externalUserId,
+ selectedComponentType,
+ editableExternalUserId,
+ setEditableExternalUserId,
+ accountId,
+ setAccountId
+}: AuthSectionProps) => (
-
-
User ID
-
-
-
-
- {externalUserId}
-
-
-
- {externalUserId}
-
-
-
+
+
+
User ID
+ {selectedComponentType === "proxy" ? (
+
setEditableExternalUserId?.(e.target.value)}
+ placeholder="Enter external user ID"
+ className="text-[13px] font-mono h-8"
+ />
+ ) : (
+
+
+
+
+ {externalUserId}
+
+
+
+ {externalUserId}
+
+
+
+ )}
+
+
+ {selectedComponentType === "proxy" && (
+
+
Account ID
+
setAccountId?.(e.target.value)}
+ placeholder="Enter account ID"
+ className="text-[13px] font-mono h-8"
+ />
+
+ )}
)
diff --git a/connect-react-demo/lib/app-state.tsx b/connect-react-demo/lib/app-state.tsx
index ba99b97..d4c1f2b 100644
--- a/connect-react-demo/lib/app-state.tsx
+++ b/connect-react-demo/lib/app-state.tsx
@@ -82,6 +82,14 @@ const useAppStateProviderValue = () => {
const [webhookUrl, setWebhookUrl] = useState
("")
const [webhookUrlValidationAttempted, setWebhookUrlValidationAttempted] = useState(false)
+ // Proxy-specific state
+ const [editableExternalUserId, setEditableExternalUserId] = useState(externalUserId)
+ const [accountId, setAccountId] = useState("")
+ const [proxyUrl, setProxyUrl] = useState("")
+ const [proxyPath, setProxyPath] = useState("")
+ const [proxyMethod, setProxyMethod] = useState("GET")
+ const [proxyBody, setProxyBody] = useState("")
+
const selectedComponentKey = queryParams.component || "google_sheets-add-single-row"
const setSelectedComponentKey = (value: string) => {
// Batch all state updates to prevent multiple configureComponent calls
@@ -221,6 +229,18 @@ export function MyPage() {
fileCode,
setFileCode,
+ // Proxy-specific exports
+ editableExternalUserId,
+ setEditableExternalUserId,
+ accountId,
+ setAccountId,
+ proxyUrl,
+ setProxyUrl,
+ proxyMethod,
+ setProxyMethod,
+ proxyBody,
+ setProxyBody,
+
code,
}
}