Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions connect-react-demo/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ PIPEDREAM_PROJECT_ID=<your-project-id> # Starts with 'proj_', available in your
PIPEDREAM_PROJECT_ENVIRONMENT=development
PIPEDREAM_ALLOWED_ORIGINS='["https://example.com", "http://localhost:3000"]'
#NEXT_PUBLIC_EXTERNAL_USER_ID= # Set a static external-user-id here for easier debugging
#NEXT_PUBLIC_ENABLE_PROXY_INPUT=true # Allow editing externalUserId and accountId fields in proxy mode
60 changes: 60 additions & 0 deletions connect-react-demo/app/actions/backendClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,3 +40,55 @@ 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);

// Log the response structure for debugging
console.log('Proxy response structure:', {
resp,
type: typeof resp,
keys: Object.keys(resp || {}),
hasHeaders: !!(resp as any)?.headers,
hasStatus: !!(resp as any)?.status
});

// Return both the response data and any available metadata (like headers)
// Note: The Pipedream SDK might return headers differently
return {
data: resp,
headers: (resp as any)?.headers || (resp as any)?.response?.headers || {},
status: (resp as any)?.status || (resp as any)?.response?.status || 200,
rawResponse: resp // Include raw response for debugging
}
} 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
14 changes: 10 additions & 4 deletions connect-react-demo/app/components/ComponentTypeSelector.tsx
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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) {
Expand All @@ -42,7 +48,7 @@ export function ComponentTypeSelector({ selectedType, onTypeChange }: ComponentT
<type.icon className="h-3 w-3" />
{type.label}
</button>
{index === 0 && <div className={TOGGLE_STYLES.separator} />}
{index < COMPONENT_TYPES.length - 1 && <div className={TOGGLE_STYLES.separator} />}
</div>
))}
</div>
Expand Down
209 changes: 143 additions & 66 deletions connect-react-demo/app/components/ConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,14 @@ export const ConfigPanel = () => {
propNames,
webhookUrlValidationAttempted,
setWebhookUrlValidationAttempted,
editableExternalUserId,
setEditableExternalUserId,
accountId,
setAccountId,
} = useAppState()

// Check if proxy input editing is enabled via environment variable
const enableProxyInput = process.env.NEXT_PUBLIC_ENABLE_PROXY_INPUT === 'true'
const id1 = useId();
const id2 = useId();
const [showAdvanced, setShowAdvanced] = useState(false);
Expand Down Expand Up @@ -305,7 +312,7 @@ export const ConfigPanel = () => {
<div className="pb-1.5 mb-1.5 border-b border-neutral-200 font-medium">
<span className="text-[#d73a49]">type</span>{" "}
<span className="text-[#6f42c1]">componentType</span> ={" "}
<span className="text-[#d73a49]">'action' | 'trigger'</span>
<span className="text-[#d73a49]">'action' | 'trigger' | 'proxy'</span>
</div>

<div className="font-sans text-neutral-600 py-1 text-[13px] leading-normal font-normal">
Expand Down Expand Up @@ -334,48 +341,55 @@ export const ConfigPanel = () => {
/>
</div>
</div>
<PropertyItem
name="app"
type="string"
description="App to connect to"
required={true}
>
<CustomizeProvider customization={dropdownCustomization}>
<SelectApp
value={selectedApp}
onChange={(app) => {
app
? setSelectedAppSlug(app.name_slug)
: removeSelectedAppSlug()
}}
/>
</CustomizeProvider>
</PropertyItem>
<PropertyItem
name={selectedComponentType === "action" ? "actionId" : "triggerId"}
type="string"
description={`${selectedComponentType === "action" ? "Action" : "Trigger"} to use`}
required={true}
>
<CustomizeProvider customization={dropdownCustomization}>
{selectedApp ? (
<SelectComponent
app={selectedApp}
componentType={selectedComponentType}
value={selectedComponent}
onChange={(comp) => {
comp
? setSelectedComponentKey(comp.key)
: removeSelectedComponentKey()
{(selectedComponentType === "action" || selectedComponentType === "trigger" || selectedComponentType === "proxy") && (
<PropertyItem
name="app"
type="string"
description="App to connect to"
required={true}
>
<CustomizeProvider customization={dropdownCustomization}>
<SelectApp
value={selectedApp}
onChange={(app) => {
if (app) {
console.log('📱 App selected:', app)
setSelectedAppSlug(app.name_slug)
} else {
removeSelectedAppSlug()
}
}}
/>
) : (
<div className="w-full px-3 py-1.5 text-sm text-gray-500 border rounded bg-gray-50">
Loading components...
</div>
)}
</CustomizeProvider>
</PropertyItem>
</CustomizeProvider>
</PropertyItem>
)}
{selectedComponentType !== "proxy" && (
<PropertyItem
name={selectedComponentType === "action" ? "actionId" : "triggerId"}
type="string"
description={`${selectedComponentType === "action" ? "Action" : "Trigger"} to use`}
required={true}
>
<CustomizeProvider customization={dropdownCustomization}>
{selectedApp ? (
<SelectComponent
app={selectedApp}
componentType={selectedComponentType}
value={selectedComponent}
onChange={(comp) => {
comp
? setSelectedComponentKey(comp.key)
: removeSelectedComponentKey()
}}
/>
) : (
<div className="w-full px-3 py-1.5 text-sm text-gray-500 border rounded bg-gray-50">
Loading components...
</div>
)}
</CustomizeProvider>
</PropertyItem>
)}
{selectedComponentType === "trigger" && (
<PropertyItem
name="webhookUrl"
Expand Down Expand Up @@ -424,12 +438,71 @@ export const ConfigPanel = () => {
description="Authenticated user identifier"
required={true}
>
<input
value={externalUserId || ""}
className="w-full px-3 py-1.5 text-sm font-mono border rounded bg-zinc-50/50"
readOnly
/>
{selectedComponentType === "proxy" ? (
<input
value={editableExternalUserId || ""}
onChange={enableProxyInput ? (e) => setEditableExternalUserId(e.target.value) : undefined}
placeholder={enableProxyInput ? "Enter external user ID" : "External user ID (read-only)"}
className={`w-full px-3 py-1.5 text-sm font-mono border rounded ${
enableProxyInput
? "bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500"
: "bg-zinc-50/50"
}`}
readOnly={!enableProxyInput}
/>
) : (
<input
value={externalUserId || ""}
className="w-full px-3 py-1.5 text-sm font-mono border rounded bg-zinc-50/50"
readOnly
/>
)}
</PropertyItem>
{selectedApp && (
<div className="grid grid-cols-[120px_1fr] gap-3 py-2 pl-4 pr-2 hover:bg-zinc-50/50">
<div className="flex items-start pt-1.5">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<label className="text-[13px] font-semibold text-neutral-500 border-b border-dotted border-neutral-300 cursor-help">
app metadata
</label>
</TooltipTrigger>
<TooltipContent
side="right"
className="animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 flex flex-col gap-1.5 max-w-xs bg-white border border-neutral-200 shadow-sm text-neutral-700 rounded-md p-2.5 font-mono text-[13px] leading-tight tracking-tight"
>
<div className="font-sans text-neutral-600 py-1 text-[13px] leading-normal font-normal">
Complete metadata for the selected app
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex items-start">
<div className="w-full p-3 bg-gray-50 border border-gray-200 rounded text-xs font-mono">
<pre className="text-gray-700 whitespace-pre-wrap">
{JSON.stringify(selectedApp, null, 2)}
</pre>
</div>
</div>
</div>
)}
{selectedComponentType === "proxy" && enableProxyInput && (
<PropertyItem
name="accountId"
type="string"
description="Account ID for authenticated requests"
required={true}
>
<input
value={accountId || ""}
onChange={(e) => 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"
/>
</PropertyItem>
)}
</div>
)

Expand Down Expand Up @@ -566,29 +639,33 @@ export const ConfigPanel = () => {
{basicFormControls}

{/* Desktop: Show with section header */}
<div className="hidden md:block mt-6">
<div className="border-t border-gray-200 pt-6">
<h3 className="text-sm font-medium text-gray-700 mb-4">Additional Config Options</h3>
{advancedFormControls}
{selectedComponentType !== "proxy" && (
<div className="hidden md:block mt-6">
<div className="border-t border-gray-200 pt-6">
<h3 className="text-sm font-medium text-gray-700 mb-4">Additional Config Options</h3>
{advancedFormControls}
</div>
</div>
</div>
)}

{/* Mobile: Collapsible */}
<div className="md:hidden mt-4">
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
<CollapsibleTrigger className="flex items-center justify-between w-full py-2 px-3 text-sm font-medium text-neutral-500 hover:text-neutral-600 hover:bg-neutral-25 rounded border border-neutral-150">
<div className="flex items-center gap-2">
<IoSettingsOutline className="h-4 w-4" />
More options
</div>
<IoChevronDown className={cn("h-4 w-4 transition-transform", showAdvanced && "rotate-180")} />
</CollapsibleTrigger>

<CollapsibleContent>
{advancedFormControls}
</CollapsibleContent>
</Collapsible>
</div>
{selectedComponentType !== "proxy" && (
<div className="md:hidden mt-4">
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
<CollapsibleTrigger className="flex items-center justify-between w-full py-2 px-3 text-sm font-medium text-neutral-500 hover:text-neutral-600 hover:bg-neutral-25 rounded border border-neutral-150">
<div className="flex items-center gap-2">
<IoSettingsOutline className="h-4 w-4" />
More options
</div>
<IoChevronDown className={cn("h-4 w-4 transition-transform", showAdvanced && "rotate-180")} />
</CollapsibleTrigger>

<CollapsibleContent>
{advancedFormControls}
</CollapsibleContent>
</Collapsible>
</div>
)}

{triggerInfo}
</div>
Expand Down
Loading