feat: CORS enable button...ish still now understanding this

This commit is contained in:
Zakaria
2026-09-27 14:46:04 -04:00
parent 8470b8d17a
commit b474e823ce
17 changed files with 943 additions and 31 deletions
@@ -1,18 +1,20 @@
import {useEffect, useState} from 'react'
import {ArrowRight, Braces, Copy, Link2, ListTree, Loader2, Rocket, Send, Trash2, Waypoints, X} from 'lucide-react'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {createApiGatewayLambdaRoute, deleteApiGatewayRoute, listApiGatewayRoutes, listCloudResources, testApiGatewayRoute} from '@/api/cloudProxyClient'
import {createApiGatewayLambdaRoute, deleteApiGatewayRoute, enableApiGatewayCors, listApiGatewayRoutes, listCloudResources, testApiGatewayRoute} from '@/api/cloudProxyClient'
import type {CloudProvider} from '@/types/cloud'
import type {CloudResource} from '@/types/resource'
import {CopyValueButton} from './CopyValueButton'
import {LogsQueryPanel} from './LogsQueryPanel'
interface ApiGatewayRoutePanelProps {
cloud: CloudProvider
resource?: CloudResource
runtimeReachable: boolean
runtimeEndpoint?: string
}
export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGatewayRoutePanelProps) {
export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtimeEndpoint}: ApiGatewayRoutePanelProps) {
const [path, setPath] = useState('/hello')
const [method, setMethod] = useState('GET')
const [stage, setStage] = useState('dev')
@@ -24,6 +26,11 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
const [bodyFormatError, setBodyFormatError] = useState<string | null>(null)
const [logsRefreshKey, setLogsRefreshKey] = useState(0)
const [deleteConfirmation, setDeleteConfirmation] = useState<string | null>(null)
const [corsConfirmation, setCorsConfirmation] = useState<{resourceId: string; path: string} | null>(null)
const [corsOrigin, setCorsOrigin] = useState('*')
const [corsHeaders, setCorsHeaders] = useState('Content-Type')
const [corsMethods, setCorsMethods] = useState<string[]>([])
const [corsProxyMethods, setCorsProxyMethods] = useState<string[]>([])
const queryClient = useQueryClient()
const functionsQuery = useQuery({
queryKey: ['cloud-resources', cloud, 'serverless'],
@@ -48,6 +55,11 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
setBodyFormatError(null)
setLogsRefreshKey(0)
setDeleteConfirmation(null)
setCorsConfirmation(null)
setCorsOrigin('*')
setCorsHeaders('Content-Type')
setCorsMethods([])
setCorsProxyMethods([])
}, [resource?.id])
const routeMutation = useMutation({
@@ -73,6 +85,20 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
queryClient.invalidateQueries({queryKey: ['api-gateway-routes', cloud, resource!.id]})
},
})
const corsMutation = useMutation({
mutationFn: () => enableApiGatewayCors(cloud, resource!.id, {
resourceId: corsConfirmation!.resourceId,
stage,
origin: corsOrigin,
headers: corsHeaders,
methods: corsMethods,
}),
onSuccess: (result) => {
setCorsProxyMethods(result.proxyMethods)
setCorsConfirmation(null)
queryClient.invalidateQueries({queryKey: ['api-gateway-routes', cloud, resource!.id]})
},
})
if (!resource || resource.service !== 'apigateway') {
return (
@@ -89,6 +115,8 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
const canCreate = cloud === 'aws' && runtimeReachable && Boolean(functionName) && !routeMutation.isPending
const existingPath = routesQuery.data?.routes.some((route) => route.path === path) ?? false
const curlCommand = buildCurlCommand(resource.id, stage, path, method, testHeaders, testQuery, testBody)
const endpoint = invokeEndpoint(runtimeEndpoint)
const baseInvokeUrl = endpoint ? `${endpoint}/execute-api/${resource.id}/${stage}` : null
return (
<section className="table-panel">
@@ -108,6 +136,13 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
)}
</div>
<div className="resource-create-inline">
{baseInvokeUrl && <div className="dynamic-field">
<span>Base invoke URL</span>
<div className="copy-value">
<code>{baseInvokeUrl}</code>
<CopyValueButton value={baseInvokeUrl} label="Base invoke URL"/>
</div>
</div>}
<form className="inspector-action-form apigateway-route-form" onSubmit={(event) => {
event.preventDefault()
if (canCreate) routeMutation.mutate()
@@ -161,12 +196,16 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
{routesQuery.isLoading && <p className="muted compact-text">Loading deployed routes...</p>}
{routesQuery.isError && <p className="error-text compact-text">{routesQuery.error instanceof Error ? routesQuery.error.message : 'Unable to load API routes.'}</p>}
{routesQuery.data?.routes.length === 0 && <p className="muted compact-text">No deployed methods yet. Create and deploy a route above.</p>}
{routesQuery.data && routesQuery.data.routes.length > 0 && (
<div className="apigateway-route-list">
{routesQuery.data.routes.map((route) => {
{routesQuery.data && routesQuery.data.routes.length > 0 && (
<div className="apigateway-route-list">
{routesQuery.data.routes.map((route) => {
const selected = route.path === path && route.method === method
const key = `${route.resourceId}-${route.method}`
return <div className={`apigateway-route-item${selected ? ' selected' : ''}`} key={key}>
const resourceMethods = routesQuery.data.routes
.filter((item) => item.resourceId === route.resourceId && CORS_METHODS.includes(item.method))
.map((item) => item.method)
const isFirstForResource = routesQuery.data.routes.findIndex((item) => item.resourceId === route.resourceId) === routesQuery.data.routes.indexOf(route)
return <div className={`apigateway-route-item${selected ? ' selected' : ''}`} key={key}>
<button className="apigateway-route-select" type="button" onClick={() => {
setPath(route.path)
setMethod(route.method)
@@ -176,18 +215,34 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
}}>
<span className="apigateway-route-method">{route.method}</span>
<code>{route.path}</code>
<small>{route.functionName ?? 'No Lambda integration'}</small>
</button>
{deleteConfirmation === key ? <div className="apigateway-route-confirm">
<small>{route.integrationType ?? 'No integration'}{route.functionName ? ` · ${route.functionName}` : ''}</small>
</button>
{isFirstForResource && <button className="button compact" type="button" onClick={() => {
setCorsConfirmation({resourceId: route.resourceId, path: route.path})
setCorsMethods(resourceMethods)
corsMutation.reset()
}}>Enable CORS</button>}
{deleteConfirmation === key ? <div className="apigateway-route-confirm">
<span>Delete {route.method}?</span>
<button className="button danger compact" type="button" disabled={deleteRouteMutation.isPending} onClick={() => deleteRouteMutation.mutate(route)}>Delete</button>
<button className="button compact" type="button" disabled={deleteRouteMutation.isPending} onClick={() => setDeleteConfirmation(null)}>Cancel</button>
</div> : <button className="icon-btn" type="button" aria-label={`Delete ${route.method} ${route.path}`} onClick={() => setDeleteConfirmation(key)}><Trash2 size={13}/></button>}
</div>
</div> : <button className="icon-btn" type="button" aria-label={`Delete ${route.method} ${route.path}`} onClick={() => setDeleteConfirmation(key)}><Trash2 size={13}/></button>}
{corsConfirmation?.resourceId === route.resourceId && isFirstForResource && <div className="apigateway-route-confirm">
<strong>Enable CORS for {corsConfirmation.path}</strong>
<label className="dynamic-field"><span>Allowed origin</span><input className="input" value={corsOrigin} onChange={(event) => setCorsOrigin(event.target.value)}/></label>
<label className="dynamic-field"><span>Allowed headers</span><input className="input" value={corsHeaders} onChange={(event) => setCorsHeaders(event.target.value)}/></label>
<div className="dynamic-field"><span>Allowed methods</span>{resourceMethods.map((value) => <label key={value}><input type="checkbox" checked={corsMethods.includes(value)} onChange={() => setCorsMethods((current) => current.includes(value) ? current.filter((method) => method !== value) : [...current, value])}/>{value}</label>)}</div>
<span>Updates this resource's OPTIONS preflight and non-proxy method responses, then deploys {stage}.</span>
<button className="button primary compact" type="button" disabled={corsMutation.isPending || corsMethods.length === 0} onClick={() => corsMutation.mutate()}>{corsMutation.isPending ? 'Enabling' : 'Confirm enable CORS'}</button>
<button className="button compact" type="button" disabled={corsMutation.isPending} onClick={() => setCorsConfirmation(null)}>Cancel</button>
{corsMutation.isError && <p className="error-text compact-text">{corsMutation.error instanceof Error ? corsMutation.error.message : 'Unable to enable CORS.'}</p>}
</div>}
</div>
})}
</div>
)}
</div>
</div>
)}
{corsProxyMethods.length > 0 && <ProxyCorsWarning methods={corsProxyMethods} origin={corsOrigin} headers={corsHeaders}/>}
</div>
<div className="apigateway-test-panel">
<div className="dynamic-stage-header">
<div>
@@ -299,6 +354,45 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable}: ApiGat
)
}
const CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
function ProxyCorsWarning({methods, origin, headers}: {methods: string[]; origin: string; headers: string}) {
return <div className="error-text compact-text">
<strong>AWS_PROXY methods need their backend to return CORS headers: {methods.join(', ')}.</strong>
<pre>{`import "net/http"
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "${origin}")
w.Header().Set("Access-Control-Allow-Headers", "${headers}")
w.Header().Set("Access-Control-Allow-Methods", "${methods.join(',')},OPTIONS")
if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK); return }
// Handle the proxy request.
}`}</pre>
</div>
}
function invokeEndpoint(runtimeEndpoint?: string): string | null {
if (!runtimeEndpoint) return null
try {
const endpoint = new URL(runtimeEndpoint)
if (isDockerBridgeAddress(endpoint.hostname) && typeof window !== 'undefined') {
endpoint.hostname = window.location.hostname
}
return endpoint.toString().replace(/\/$/, '')
} catch {
return runtimeEndpoint.replace(/\/$/, '')
}
}
function isDockerBridgeAddress(hostname: string): boolean {
const octets = hostname.split('.').map(Number)
return octets.length === 4
&& octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255)
&& octets[0] === 172
&& octets[1] >= 16
&& octets[1] <= 31
}
function parseHeaders(value: string): Record<string, string> {
let parsed: unknown
try {