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; };