From 2d4683d5bf6122223eb286abd7632ff608328aa2 Mon Sep 17 00:00:00 2001
From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com>
Date: Fri, 5 Dec 2025 00:34:08 +0100
Subject: [PATCH] feat(vercel): always show all integration steps
---
.../connect-manual-serverfull-frame.tsx | 150 +--------------
.../connect-manual-serverless-frame.tsx | 9 +-
.../dialogs/connect-quick-vercel-frame.tsx | 9 +-
.../src/app/dialogs/connect-railway-frame.tsx | 156 +---------------
.../src/app/dialogs/connect-vercel-frame.tsx | 27 +--
frontend/src/app/env-variables.tsx | 172 ++++++++++++++++++
.../app/forms/connect-quick-vercel-form.tsx | 5 +-
.../src/app/forms/connect-vercel-form.tsx | 15 +-
frontend/src/app/forms/stepper-form.tsx | 119 ++++++++----
.../src/components/actors/data-provider.tsx | 1 -
10 files changed, 293 insertions(+), 370 deletions(-)
create mode 100644 frontend/src/app/env-variables.tsx
diff --git a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx
index df30f0d708..e788b4ba5f 100644
--- a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx
+++ b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx
@@ -10,18 +10,12 @@ import { useWatch } from "react-hook-form";
import { match } from "ts-pattern";
import z from "zod";
import * as ConnectRailwayForm from "@/app/forms/connect-manual-serverfull-form";
-import {
- Button,
- CopyButton,
- type DialogContentProps,
- DiscreteInput,
- Label,
- Skeleton,
-} from "@/components";
+import type { DialogContentProps } from "@/components";
import { useEngineCompatDataProvider } from "@/components/actors";
import { defineStepper } from "@/components/ui/stepper";
import { engineEnv } from "@/lib/env";
import { queryClient } from "@/queries/global";
+import { EnvVariables } from "../env-variables";
import { StepperForm } from "../forms/stepper-form";
const stepper = defineStepper(
@@ -164,60 +158,6 @@ function FormStepper({
);
}
-export function EnvVariablesStep() {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {
- const inputs =
- document.querySelectorAll(
- "[data-env-variables] input",
- );
- return Array.from(inputs)
- .reduce((acc, input, index) => {
- if (index % 2 === 0) {
- acc.push(
- `${input.value}=${inputs[index + 1]?.value}`,
- );
- }
- return acc;
- }, [] as string[])
- .join("\n");
- }}
- >
-
-
-
-
- >
- );
-}
-
function Step1() {
return (
<>
@@ -281,7 +221,10 @@ function Step2({ provider }: { provider: string }) {
Set the following environment variables.
))}
-
+
>
);
}
@@ -290,86 +233,7 @@ function Step3() {
return ;
}
-function RivetRunnerEnv() {
- const runnerName = useWatch({ name: "runnerName" });
- if (runnerName === "default") return null;
-
- return (
- <>
-
-
- >
- );
-}
-
-function RivetTokenEnv() {
- const { data, isLoading } = useQuery(
- useEngineCompatDataProvider().engineAdminTokenQueryOptions(),
- );
- return (
- <>
-
- {isLoading ? (
-
- ) : (
-
- )}
- >
- );
-}
-
-function RivetEndpointEnv() {
- const url = useSelectedDatacenter();
- return (
- <>
-
-
- >
- );
-}
-
-function RivetNamespaceEnv() {
- const dataProvider = useEngineCompatDataProvider();
- return (
- <>
-
-
- >
- );
-}
-
-const useSelectedDatacenter = () => {
+export const useSelectedDatacenter = () => {
const datacenter = useWatch({ name: "datacenter" });
const { data } = useQuery(
diff --git a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx
index 71e32cb2f1..c94e1cfe63 100644
--- a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx
+++ b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx
@@ -5,14 +5,16 @@ import {
useSuspenseInfiniteQuery,
} from "@tanstack/react-query";
import confetti from "canvas-confetti";
+import { useWatch } from "react-hook-form";
import z from "zod";
import * as ConnectServerlessForm from "@/app/forms/connect-manual-serverless-form";
import type { DialogContentProps } from "@/components";
import { type Region, useEngineCompatDataProvider } from "@/components/actors";
import { defineStepper } from "@/components/ui/stepper";
import { queryClient } from "@/queries/global";
+import { EnvVariables } from "../env-variables";
import { StepperForm } from "../forms/stepper-form";
-import { EnvVariablesStep } from "./connect-railway-frame";
+import { useSelectedDatacenter } from "./connect-manual-serverfull-frame";
const stepper = defineStepper(
{
@@ -199,7 +201,10 @@ function Step2() {
return (
<>
Set the following environment variables.
-
+
>
);
}
diff --git a/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx b/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx
index 4acd3a570f..7d27a9b877 100644
--- a/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx
+++ b/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx
@@ -20,10 +20,7 @@ import {
import { type Region, useEngineCompatDataProvider } from "@/components/actors";
import { queryClient } from "@/queries/global";
import { StepperForm } from "../forms/stepper-form";
-import {
- EnvVariablesStep,
- useSelectedDatacenter,
-} from "./connect-railway-frame";
+import { useSelectedDatacenter } from "./connect-manual-serverfull-frame";
import { VERCEL_SERVERLESS_MAX_DURATION } from "./connect-vercel-frame";
const { stepper } = ConnectVercelForm;
@@ -89,6 +86,8 @@ function FormStepper({
return (
,
deploy: () => ,
@@ -178,7 +177,7 @@ function StepInitialInfo() {
Set the following environment variables:
-
+
>
);
diff --git a/frontend/src/app/dialogs/connect-railway-frame.tsx b/frontend/src/app/dialogs/connect-railway-frame.tsx
index 13f0615ac8..583714263b 100644
--- a/frontend/src/app/dialogs/connect-railway-frame.tsx
+++ b/frontend/src/app/dialogs/connect-railway-frame.tsx
@@ -14,20 +14,17 @@ import {
AccordionContent,
AccordionItem,
AccordionTrigger,
- Button,
- CopyButton,
type DialogContentProps,
- DiscreteInput,
Frame,
- Label,
- Skeleton,
} from "@/components";
import { useEngineCompatDataProvider } from "@/components/actors";
import { defineStepper } from "@/components/ui/stepper";
import { engineEnv } from "@/lib/env";
import { queryClient } from "@/queries/global";
import { useRailwayTemplateLink } from "@/utils/use-railway-template-link";
+import { EnvVariables } from "../env-variables";
import { StepperForm } from "../forms/stepper-form";
+import { useSelectedDatacenter } from "./connect-manual-serverfull-frame";
const stepper = defineStepper(
{
@@ -156,60 +153,6 @@ function FormStepper({
);
}
-export function EnvVariablesStep() {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {
- const inputs =
- document.querySelectorAll(
- "[data-env-variables] input",
- );
- return Array.from(inputs)
- .reduce((acc, input, index) => {
- if (index % 2 === 0) {
- acc.push(
- `${input.value}=${inputs[index + 1]?.value}`,
- );
- }
- return acc;
- }, [] as string[])
- .join("\n");
- }}
- >
-
-
-
-
- >
- );
-}
-
function Step1() {
return (
<>
@@ -242,7 +185,10 @@ function Step2() {
Set the following environment variables in your Railway project
settings.
-
+
>
);
}
@@ -251,86 +197,6 @@ function Step3() {
return ;
}
-function RivetRunnerEnv() {
- const runnerName = useWatch({ name: "runnerName" });
- if (runnerName === "default") return null;
-
- return (
- <>
-
-
- >
- );
-}
-
-function RivetTokenEnv() {
- const { data, isLoading } = useQuery(
- useEngineCompatDataProvider().engineAdminTokenQueryOptions(),
- );
- return (
- <>
-
- {isLoading ? (
-
- ) : (
-
- )}
- >
- );
-}
-
-function RivetEndpointEnv() {
- const url = useSelectedDatacenter();
- return (
- <>
-
-
- >
- );
-}
-
-function RivetNamespaceEnv() {
- const dataProvider = useEngineCompatDataProvider();
- return (
- <>
-
-
- >
- );
-}
-
function DeployToRailwayButton() {
const runnerName = useWatch({ name: "runnerName" });
@@ -354,13 +220,3 @@ function DeployToRailwayButton() {
);
}
-
-export const useSelectedDatacenter = () => {
- const datacenter = useWatch({ name: "datacenter" });
-
- const { data } = useQuery(
- useEngineCompatDataProvider().regionQueryOptions(datacenter || "auto"),
- );
-
- return data?.url || engineEnv().VITE_APP_API_URL;
-};
diff --git a/frontend/src/app/dialogs/connect-vercel-frame.tsx b/frontend/src/app/dialogs/connect-vercel-frame.tsx
index cdd1c0b2f1..0f06a0f75e 100644
--- a/frontend/src/app/dialogs/connect-vercel-frame.tsx
+++ b/frontend/src/app/dialogs/connect-vercel-frame.tsx
@@ -3,7 +3,6 @@ import {
useMutation,
usePrefetchInfiniteQuery,
useSuspenseInfiniteQuery,
- useSuspenseQuery,
} from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import confetti from "canvas-confetti";
@@ -20,14 +19,10 @@ import {
Frame,
getConfig,
} from "@/components";
-import {
- type Region,
- useCloudNamespaceDataProvider,
- useEngineCompatDataProvider,
- useEngineNamespaceDataProvider,
-} from "@/components/actors";
+import { type Region, useEngineCompatDataProvider } from "@/components/actors";
import { cloudEnv } from "@/lib/env";
import { queryClient } from "@/queries/global";
+import { usePublishableToken } from "../env-variables";
import { type JoinStepSchemas, StepperForm } from "../forms/stepper-form";
const { stepper } = ConnectVercelForm;
@@ -38,24 +33,6 @@ export const VERCEL_SERVERLESS_MAX_DURATION = 300;
interface CreateProjectFrameContentProps extends DialogContentProps {}
-function usePublishableToken() {
- const cloudProvider = useCloudNamespaceDataProvider();
- const engineProvider = useEngineNamespaceDataProvider();
- const cloudData = useSuspenseQuery(
- cloudProvider.publishableTokenQueryOptions(),
- );
- const engineData = useSuspenseQuery(
- engineProvider.engineAdminTokenQueryOptions(),
- );
-
- return match(__APP_TYPE__)
- .with("cloud", () => cloudData.data)
- .with("engine", () => engineData.data)
- .otherwise(() => {
- throw new Error("Not in a valid context");
- });
-}
-
const useEndpoint = () => {
return match(__APP_TYPE__)
.with("cloud", () => {
diff --git a/frontend/src/app/env-variables.tsx b/frontend/src/app/env-variables.tsx
new file mode 100644
index 0000000000..bdd1d9ad77
--- /dev/null
+++ b/frontend/src/app/env-variables.tsx
@@ -0,0 +1,172 @@
+import { useSuspenseQuery } from "@tanstack/react-query";
+import { useRouteContext } from "@tanstack/react-router";
+import { match } from "ts-pattern";
+import { Button, CopyButton, DiscreteInput } from "@/components";
+import {
+ useEngineCompatDataProvider,
+ useEngineNamespaceDataProvider,
+} from "@/components/actors";
+import { Label } from "@/components/ui/label";
+
+export function EnvVariables({
+ prefix,
+ runnerName,
+ endpoint,
+}: {
+ prefix?: string;
+ runnerName?: string;
+ endpoint: string;
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {
+ const inputs =
+ document.querySelectorAll(
+ "[data-env-variables] input",
+ );
+ return Array.from(inputs)
+ .reduce((acc, input, index) => {
+ if (index % 2 === 0) {
+ acc.push(
+ `${input.value}=${inputs[index + 1]?.value}`,
+ );
+ }
+ return acc;
+ }, [] as string[])
+ .join("\n");
+ }}
+ >
+
+
+
+
+ );
+}
+
+function RivetRunnerEnv({
+ prefix,
+ runnerName,
+}: {
+ prefix?: string;
+ runnerName?: string;
+}) {
+ if (runnerName === "default") return null;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function RivetTokenEnv({ prefix }: { prefix?: string }) {
+ const data = usePublishableToken();
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+function RivetEndpointEnv({
+ prefix,
+ endpoint,
+}: {
+ prefix?: string;
+ endpoint: string;
+}) {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function RivetNamespaceEnv({ prefix }: { prefix?: string }) {
+ const dataProvider = useEngineCompatDataProvider();
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export function usePublishableToken() {
+ return match(__APP_TYPE__)
+ .with("cloud", () => {
+ // biome-ignore lint/correctness/useHookAtTopLevel: guarded by the build flag
+ const routeContext = useRouteContext({
+ from: "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/connect",
+ select: (ctx) => ctx.dataProvider,
+ });
+ // biome-ignore lint/correctness/useHookAtTopLevel: guarded by the build flag
+ return useSuspenseQuery(routeContext.publishableTokenQueryOptions())
+ .data;
+ })
+ .with("engine", () => {
+ // biome-ignore lint/correctness/useHookAtTopLevel: guarded by the build flag
+ return useSuspenseQuery(
+ // biome-ignore lint/correctness/useHookAtTopLevel: guarded by the build flag
+ useEngineNamespaceDataProvider().engineAdminTokenQueryOptions(),
+ ).data;
+ })
+ .otherwise(() => {
+ throw new Error("Not in a valid context");
+ });
+}
diff --git a/frontend/src/app/forms/connect-quick-vercel-form.tsx b/frontend/src/app/forms/connect-quick-vercel-form.tsx
index 949d3430e8..e078ec63ad 100644
--- a/frontend/src/app/forms/connect-quick-vercel-form.tsx
+++ b/frontend/src/app/forms/connect-quick-vercel-form.tsx
@@ -1,8 +1,7 @@
-import { useFormContext } from "react-hook-form";
import z from "zod";
-import * as ConnectManualServerlessForm from "@/app/forms/connect-manual-serverless-form";
import * as ConnectVercelForm from "@/app/forms/connect-vercel-form";
import { defineStepper } from "@/components/ui/stepper";
+import { useSelectedDatacenter } from "../dialogs/connect-manual-serverfull-frame";
const endpointSchema = z
.string()
@@ -60,3 +59,5 @@ export const Headers = ConnectVercelForm.Headers;
export const Endpoint = ConnectVercelForm.Endpoint;
export const ConnectionCheck = ConnectVercelForm.ConnectionCheck;
+
+export const EnvVariables = ConnectVercelForm.EnvVariables;
diff --git a/frontend/src/app/forms/connect-vercel-form.tsx b/frontend/src/app/forms/connect-vercel-form.tsx
index 6593882c1a..67cf4da9e2 100644
--- a/frontend/src/app/forms/connect-vercel-form.tsx
+++ b/frontend/src/app/forms/connect-vercel-form.tsx
@@ -1,8 +1,7 @@
-import { useFormContext } from "react-hook-form";
+import { useFormContext, useWatch } from "react-hook-form";
import z from "zod";
import * as ConnectManualServerlessForm from "@/app/forms/connect-manual-serverless-form";
import {
- Code,
CodeFrame,
CodeGroup,
CodePreview,
@@ -19,6 +18,8 @@ import {
SelectValue,
} from "@/components";
import { defineStepper } from "@/components/ui/stepper";
+import { useSelectedDatacenter } from "../dialogs/connect-manual-serverfull-frame";
+import { EnvVariables as EnvVariablesSection } from "../env-variables";
const endpointSchema = z
.string()
@@ -288,3 +289,13 @@ export const { useActor } = createRivetKit({
);
};
+
+export function EnvVariables() {
+ return (
+
+ );
+}
diff --git a/frontend/src/app/forms/stepper-form.tsx b/frontend/src/app/forms/stepper-form.tsx
index 2120ebb615..ccb0849a36 100644
--- a/frontend/src/app/forms/stepper-form.tsx
+++ b/frontend/src/app/forms/stepper-form.tsx
@@ -6,6 +6,7 @@ import {
type UseFormProps,
type UseFormReturn,
useForm,
+ useFormContext,
} from "react-hook-form";
import type * as z from "zod";
import { Button } from "@/components";
@@ -48,6 +49,8 @@ type StepperFormProps = StepperProps &
stepper: ReturnType["useStepper"]>;
}) => Promise | void;
content: Record ReactNode>;
+ showAllSteps?: boolean;
+ initialStep?: Steps[number]["id"];
};
export type StepperFormValues = z.TypeOf<
@@ -63,7 +66,7 @@ export function StepperForm(
) {
const Stepper = props.Stepper;
return (
-
+
{...props} />
);
@@ -74,10 +77,12 @@ function Content({
Stepper,
useStepper,
content,
+ showAllSteps,
onSubmit,
+ initialStep,
...formProps
}: StepperFormProps) {
- const stepper = useStepper();
+ const stepper = useStepper({ initialStep });
const form = useForm>>({
defaultValues,
resolver: zodResolver(stepper.current.schema),
@@ -103,7 +108,7 @@ function Content({
return form.handleSubmit(handleSubmit)(event);
}}
>
- {stepper.all.map((step) => (
+ {stepper.all.map((step, index, steps) => (
({
>
{step.title}
- {stepper.when(step.id, (step) => {
- return (
-
- {stepper.switch(content)}
-
- {step.assist ? (
-
- ) : null}
-
-
-
-
- );
- })}
+ {showAllSteps ? (
+
+ key={step.id}
+ Stepper={Stepper}
+ stepper={stepper}
+ step={step}
+ content={content}
+ showPrevious={false}
+ showControls={steps.length - 1 === index}
+ />
+ ) : (
+ stepper.when(step.id, (step) => {
+ return (
+
+ Stepper={Stepper}
+ stepper={stepper}
+ step={step}
+ content={content}
+ />
+ );
+ })
+ )}
))}
@@ -156,6 +146,55 @@ function Content({
);
}
+function StepPanel({
+ Stepper,
+ stepper,
+ step,
+ content,
+ showPrevious,
+ showControls = false,
+}: Pick, "Stepper" | "content"> & {
+ stepper: Stepperize.Stepper;
+ step: Steps[number];
+ showControls?: boolean;
+ showPrevious?: boolean;
+}) {
+ const form = useFormContext();
+ return (
+
+ {stepper.match(step.id, content)}
+ {showControls ? (
+
+ {step.assist ? : null}
+ {showPrevious ? (
+
+ ) : null}
+
+
+ ) : null}
+
+ );
+}
+
function NeedHelpButton() {
const [open, setOpen] = useState(false);
diff --git a/frontend/src/components/actors/data-provider.tsx b/frontend/src/components/actors/data-provider.tsx
index 7659d5c107..375ff570b7 100644
--- a/frontend/src/components/actors/data-provider.tsx
+++ b/frontend/src/components/actors/data-provider.tsx
@@ -118,6 +118,5 @@ export const useEngineCompatDataProvider = () => {
return useRouteContext({
from: routePath,
- strict: false,
}).dataProvider;
};