diff --git a/apps/dev-playground/client/src/routeTree.gen.ts b/apps/dev-playground/client/src/routeTree.gen.ts index c4c38d14..8a24acb6 100644 --- a/apps/dev-playground/client/src/routeTree.gen.ts +++ b/apps/dev-playground/client/src/routeTree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as TypeSafetyRouteRouteImport } from './routes/type-safety.route' import { Route as TelemetryRouteRouteImport } from './routes/telemetry.route' import { Route as SqlHelpersRouteRouteImport } from './routes/sql-helpers.route' +import { Route as SidecarRouteRouteImport } from './routes/sidecar.route' import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route' import { Route as LakebaseRouteRouteImport } from './routes/lakebase.route' import { Route as GenieRouteRouteImport } from './routes/genie.route' @@ -37,6 +38,11 @@ const SqlHelpersRouteRoute = SqlHelpersRouteRouteImport.update({ path: '/sql-helpers', getParentRoute: () => rootRouteImport, } as any) +const SidecarRouteRoute = SidecarRouteRouteImport.update({ + id: '/sidecar', + path: '/sidecar', + getParentRoute: () => rootRouteImport, +} as any) const ReconnectRouteRoute = ReconnectRouteRouteImport.update({ id: '/reconnect', path: '/reconnect', @@ -93,6 +99,7 @@ export interface FileRoutesByFullPath { '/genie': typeof GenieRouteRoute '/lakebase': typeof LakebaseRouteRoute '/reconnect': typeof ReconnectRouteRoute + '/sidecar': typeof SidecarRouteRoute '/sql-helpers': typeof SqlHelpersRouteRoute '/telemetry': typeof TelemetryRouteRoute '/type-safety': typeof TypeSafetyRouteRoute @@ -107,6 +114,7 @@ export interface FileRoutesByTo { '/genie': typeof GenieRouteRoute '/lakebase': typeof LakebaseRouteRoute '/reconnect': typeof ReconnectRouteRoute + '/sidecar': typeof SidecarRouteRoute '/sql-helpers': typeof SqlHelpersRouteRoute '/telemetry': typeof TelemetryRouteRoute '/type-safety': typeof TypeSafetyRouteRoute @@ -122,6 +130,7 @@ export interface FileRoutesById { '/genie': typeof GenieRouteRoute '/lakebase': typeof LakebaseRouteRoute '/reconnect': typeof ReconnectRouteRoute + '/sidecar': typeof SidecarRouteRoute '/sql-helpers': typeof SqlHelpersRouteRoute '/telemetry': typeof TelemetryRouteRoute '/type-safety': typeof TypeSafetyRouteRoute @@ -138,6 +147,7 @@ export interface FileRouteTypes { | '/genie' | '/lakebase' | '/reconnect' + | '/sidecar' | '/sql-helpers' | '/telemetry' | '/type-safety' @@ -152,6 +162,7 @@ export interface FileRouteTypes { | '/genie' | '/lakebase' | '/reconnect' + | '/sidecar' | '/sql-helpers' | '/telemetry' | '/type-safety' @@ -166,6 +177,7 @@ export interface FileRouteTypes { | '/genie' | '/lakebase' | '/reconnect' + | '/sidecar' | '/sql-helpers' | '/telemetry' | '/type-safety' @@ -181,6 +193,7 @@ export interface RootRouteChildren { GenieRouteRoute: typeof GenieRouteRoute LakebaseRouteRoute: typeof LakebaseRouteRoute ReconnectRouteRoute: typeof ReconnectRouteRoute + SidecarRouteRoute: typeof SidecarRouteRoute SqlHelpersRouteRoute: typeof SqlHelpersRouteRoute TelemetryRouteRoute: typeof TelemetryRouteRoute TypeSafetyRouteRoute: typeof TypeSafetyRouteRoute @@ -209,6 +222,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SqlHelpersRouteRouteImport parentRoute: typeof rootRouteImport } + '/sidecar': { + id: '/sidecar' + path: '/sidecar' + fullPath: '/sidecar' + preLoaderRoute: typeof SidecarRouteRouteImport + parentRoute: typeof rootRouteImport + } '/reconnect': { id: '/reconnect' path: '/reconnect' @@ -285,6 +305,7 @@ const rootRouteChildren: RootRouteChildren = { GenieRouteRoute: GenieRouteRoute, LakebaseRouteRoute: LakebaseRouteRoute, ReconnectRouteRoute: ReconnectRouteRoute, + SidecarRouteRoute: SidecarRouteRoute, SqlHelpersRouteRoute: SqlHelpersRouteRoute, TelemetryRouteRoute: TelemetryRouteRoute, TypeSafetyRouteRoute: TypeSafetyRouteRoute, diff --git a/apps/dev-playground/client/src/routes/__root.tsx b/apps/dev-playground/client/src/routes/__root.tsx index 5cf74ce3..98395c95 100644 --- a/apps/dev-playground/client/src/routes/__root.tsx +++ b/apps/dev-playground/client/src/routes/__root.tsx @@ -104,6 +104,14 @@ function RootComponent() { Files + + + diff --git a/apps/dev-playground/client/src/routes/index.tsx b/apps/dev-playground/client/src/routes/index.tsx index e331d93c..7dcc2641 100644 --- a/apps/dev-playground/client/src/routes/index.tsx +++ b/apps/dev-playground/client/src/routes/index.tsx @@ -218,6 +218,25 @@ function IndexRoute() { + + +
+

+ Sidecar +

+

+ Run polyglot child processes alongside Node.js. Compare HTTP + proxy and STDIO JSON-RPC 2.0 communication modes with live + Python sidecars. +

+ +
+
diff --git a/apps/dev-playground/client/src/routes/sidecar.route.tsx b/apps/dev-playground/client/src/routes/sidecar.route.tsx new file mode 100644 index 00000000..50dece51 --- /dev/null +++ b/apps/dev-playground/client/src/routes/sidecar.route.tsx @@ -0,0 +1,334 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Separator, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Textarea, +} from "@databricks/appkit-ui/react"; +import { createFileRoute, retainSearchParams } from "@tanstack/react-router"; +import { useCallback, useState } from "react"; +import { Header } from "@/components/layout/header"; + +export const Route = createFileRoute("/sidecar")({ + component: SidecarRoute, + search: { + middlewares: [retainSearchParams(true)], + }, +}); + +// ── Types ──────────────────────────────────────────────────────────── +interface RequestEntry { + id: number; + method: string; + path: string; + status: "pending" | "success" | "error"; + statusCode?: number; + body?: string; + duration?: number; + error?: string; + timestamp: Date; +} + +// ── Helpers ────────────────────────────────────────────────────────── +let requestId = 0; + +function statusBadgeVariant( + status: RequestEntry["status"], +): "default" | "destructive" | "secondary" { + if (status === "success") return "default"; + if (status === "error") return "destructive"; + return "secondary"; +} + +// ── Sidecar Panel (shared by both tabs) ────────────────────────────── +function SidecarPanel({ + mode, + basePath, + endpoints, +}: { + mode: "http" | "stdio"; + basePath: string; + endpoints: { + label: string; + method: string; + path: string; + hasBody?: boolean; + defaultBody?: string; + }[]; +}) { + const [history, setHistory] = useState([]); + const [customBody, setCustomBody] = useState("{}"); + + const sendRequest = useCallback( + async (method: string, path: string, body?: string) => { + const id = ++requestId; + const entry: RequestEntry = { + id, + method, + path, + status: "pending", + timestamp: new Date(), + }; + setHistory((prev) => [entry, ...prev]); + + const start = performance.now(); + try { + const opts: RequestInit = { method }; + if (body) { + opts.headers = { "Content-Type": "application/json" }; + opts.body = body; + } + const res = await fetch(`${basePath}${path}`, opts); + const duration = Math.round(performance.now() - start); + const data = await res.json(); + + setHistory((prev) => + prev.map((e) => + e.id === id + ? { + ...e, + status: res.ok ? "success" : "error", + statusCode: res.status, + body: JSON.stringify(data, null, 2), + duration, + } + : e, + ), + ); + } catch (err) { + const duration = Math.round(performance.now() - start); + setHistory((prev) => + prev.map((e) => + e.id === id + ? { + ...e, + status: "error", + error: err instanceof Error ? err.message : "Unknown error", + duration, + } + : e, + ), + ); + } + }, + [basePath], + ); + + return ( +
+ {/* Info Bar */} +
+ {mode.toUpperCase()} + {basePath}/* + {mode === "http" && ( + + Requests are proxied directly to the child HTTP server + + )} + {mode === "stdio" && ( + + Requests are translated to JSON-RPC 2.0 over stdin/stdout + + )} +
+ + {/* Actions */} + + + Endpoints + + Send requests to the{" "} + {mode === "http" ? "Python HTTP" : "Python stdio"} sidecar + + + +
+ {endpoints.map((ep) => ( + + ))} +
+ + {endpoints.some((ep) => ep.hasBody) && ( +
+ +