From 5721bf47a8f8aa86edbc025f8ac66e58d6c1db6c Mon Sep 17 00:00:00 2001 From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:32:05 +0200 Subject: [PATCH] feat: railwa connection frame --- .../data-providers/cloud-data-provider.tsx | 26 +++ .../data-providers/engine-data-provider.tsx | 14 ++ .../src/app/dialogs/connect-railway-frame.tsx | 167 +++++++++++++++++- .../src/app/dialogs/connect-vercel-frame.tsx | 6 +- .../src/app/forms/connect-railway-form.tsx | 112 +----------- .../src/app/forms/connect-vercel-form.tsx | 62 +++++-- frontend/src/components/copy-area.tsx | 39 +++- frontend/src/components/ui/stepper.tsx | 2 +- frontend/src/routes/_context/_cloud.tsx | 6 + frontend/src/routes/_context/_engine.tsx | 6 + 10 files changed, 303 insertions(+), 137 deletions(-) diff --git a/frontend/src/app/data-providers/cloud-data-provider.tsx b/frontend/src/app/data-providers/cloud-data-provider.tsx index 640ef51a55..41e426807d 100644 --- a/frontend/src/app/data-providers/cloud-data-provider.tsx +++ b/frontend/src/app/data-providers/cloud-data-provider.tsx @@ -171,6 +171,7 @@ export const createOrganizationContext = ({ }; return { + organization, orgProjectNamespacesQueryOptions, currentOrgProjectNamespacesQueryOptions: (opts: { project: string; @@ -233,6 +234,7 @@ export const createProjectContext = ({ } & ReturnType & ReturnType) => { return { + project, createNamespaceMutationOptions(opts: { onSuccess?: (data: Namespace) => void; }) { @@ -355,5 +357,29 @@ export const createNamespaceContext = ({ namespaceQueryOptions() { return parent.currentProjectNamespaceQueryOptions({ namespace }); }, + connectRunnerTokenQueryOptions() { + return queryOptions({ + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 5 * 60 * 1000, // 5 minutes + queryKey: [ + { + namespace, + project: parent.project, + organization: parent.organization, + }, + "runners", + "connect", + ], + queryFn: async () => { + const f = parent.client.namespaces.createPublishableToken( + parent.project, + namespace, + { org: parent.organization }, + ); + const t = await f; + return t.token; + }, + }); + }, }; }; diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx index f9592faa19..18282e7656 100644 --- a/frontend/src/app/data-providers/engine-data-provider.tsx +++ b/frontend/src/app/data-providers/engine-data-provider.tsx @@ -1,5 +1,6 @@ import { type Rivet, RivetClient } from "@rivetkit/engine-api-full"; import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; +import { getConfig, ls } from "@/components"; import { type Actor, ActorFeature, @@ -551,6 +552,19 @@ export const createNamespaceContext = ({ }, }); }, + connectRunnerTokenQueryOptions() { + return queryOptions({ + staleTime: 1000, + gcTime: 1000, + queryKey: [{ namespace }, "runners", "connect"], + queryFn: async () => { + return ls.engineCredentials.get(getConfig().apiUrl) || ""; + }, + meta: { + mightRequireAuth, + }, + }); + }, }; }; diff --git a/frontend/src/app/dialogs/connect-railway-frame.tsx b/frontend/src/app/dialogs/connect-railway-frame.tsx index 21481b8a9a..edeca0e899 100644 --- a/frontend/src/app/dialogs/connect-railway-frame.tsx +++ b/frontend/src/app/dialogs/connect-railway-frame.tsx @@ -1,13 +1,42 @@ import { faQuestionCircle, faRailway, Icon } from "@rivet-gg/icons"; +import { useQuery } from "@tanstack/react-query"; import * as ConnectRailwayForm from "@/app/forms/connect-railway-form"; import { HelpDropdown } from "@/app/help-dropdown"; -import { Button, Flex, Frame } from "@/components"; +import { + Button, + type DialogContentProps, + DiscreteInput, + Frame, + Skeleton, +} from "@/components"; +import { useEngineCompatDataProvider } from "@/components/actors"; +import { defineStepper } from "@/components/ui/stepper"; +import { engineEnv } from "@/lib/env"; -export default function CreateProjectFrameContent() { +const { Stepper } = defineStepper( + { + id: "step-1", + title: "Deploy to Railway", + }, + { + id: "step-2", + title: "Set Environment Variables", + }, + { + id: "step-3", + title: "Confirm Connection", + }, +); + +interface ConnectRailwayFrameContentProps extends DialogContentProps {} + +export default function ConnectRailwayFrameContent({ + onClose, +}: ConnectRailwayFrameContentProps) { return ( {}} - defaultValues={{ name: "" }} + defaultValues={{ endpoint: "" }} > @@ -22,12 +51,134 @@ export default function CreateProjectFrameContent() { - - - - - + ); } + +function FormStepper({ onClose }: { onClose?: () => void }) { + return ( + + {({ methods }) => ( + <> + + {methods.all.map((step) => ( + methods.goTo(step.id)} + > + {step.title} + {methods.when(step.id, (step) => { + return ( + + {step.id === "step-1" && ( + <> +

+ Deploy any RivetKit app + to Railway. +

+

+ Or use our Railway + template to get started + quickly. +

+ + Deploy to Railway + + +

+ After deploying your app + to Railway, return here + to add the endpoint. +

+ + )} + {step.id === "step-2" && ( + + )} + {step.id === "step-3" && ( +
+ + +
+ )} + + + + +
+ ); + })} +
+ ))} +
+ + )} +
+ ); +} + +function EnvVariablesStep() { + const dataProvider = useEngineCompatDataProvider(); + + const { data, isLoading } = useQuery( + dataProvider.connectRunnerTokenQueryOptions(), + ); + + return ( + <> +

+ Set the following environment variables in your Railway project + settings. +

+
+ {__APP_TYPE__ === "engine" ? ( +
+ + +
+ ) : null} +
+ + {isLoading ? ( + + ) : ( + + )} +
+
+ + ); +} diff --git a/frontend/src/app/dialogs/connect-vercel-frame.tsx b/frontend/src/app/dialogs/connect-vercel-frame.tsx index 0283cc7ddf..18f9c18975 100644 --- a/frontend/src/app/dialogs/connect-vercel-frame.tsx +++ b/frontend/src/app/dialogs/connect-vercel-frame.tsx @@ -5,8 +5,6 @@ import { Button, type DialogContentProps, Frame, - Step, - Steps, } from "@/components"; import { defineStepper } from "@/components/ui/stepper"; @@ -25,7 +23,7 @@ const { Stepper } = defineStepper( }, { id: "step-4", - title: "Add Vercel Endpoint", + title: "Confirm Connection", }, ); @@ -69,7 +67,7 @@ function FormStepper({ onClose }: { onClose?: () => void }) { {methods.all.map((step) => ( methods.goTo(step.id)} > diff --git a/frontend/src/app/forms/connect-railway-form.tsx b/frontend/src/app/forms/connect-railway-form.tsx index cd142e6494..7d39d75cdb 100644 --- a/frontend/src/app/forms/connect-railway-form.tsx +++ b/frontend/src/app/forms/connect-railway-form.tsx @@ -1,31 +1,10 @@ -import { faCheck, faSpinnerThird, Icon } from "@rivet-gg/icons"; -import { useQuery } from "@tanstack/react-query"; -import { useParams } from "@tanstack/react-router"; -import { AnimatePresence, motion } from "framer-motion"; -import { type UseFormReturn, useFormContext } from "react-hook-form"; +import type { UseFormReturn } from "react-hook-form"; import z from "zod"; -import { - cn, - createSchemaForm, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - Input, -} from "@/components"; -import { - useCloudDataProvider, - useEngineCompatDataProvider, -} from "@/components/actors"; +import * as ConnectVercelForm from "@/app/forms/connect-vercel-form"; +import { createSchemaForm } from "@/components"; export const formSchema = z.object({ - name: z - .string() - .max(16) - .refine((value) => value.trim() !== "" && value.trim() === value, { - message: "Name cannot be empty or contain whitespaces", - }), + endpoint: z.string().url(), }); export type FormValues = z.infer; @@ -37,84 +16,5 @@ export type SubmitHandler = ( const { Form, Submit, SetValue } = createSchemaForm(formSchema); export { Form, Submit, SetValue }; -export const Name = ({ className }: { className?: string }) => { - const { control } = useFormContext(); - return ( - ( - - Name - - - - - - )} - /> - ); -}; - -export function ConnectionCheck() { - const params = useParams({ - from: "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace", - }); - const { watch } = useFormContext(); - const name = watch("name"); - - const { data: namespace } = useQuery( - useCloudDataProvider().currentOrgProjectNamespaceQueryOptions(params), - ); - - const enabled = !!name && !!namespace?.access.engineNamespaceName; - - const { data } = useQuery({ - ...useEngineCompatDataProvider().runnerByNameQueryOptions({ - namespace: namespace?.access.engineNamespaceName || "", - runnerName: name, - }), - enabled, - refetchInterval: 1000, - }); - - const success = !!data; - - return ( - - {enabled ? ( - - {success ? ( - <> - {" "} - Runner successfully connected - - ) : ( - <> - {" "} - Waiting for runner to connect... - - )} - - ) : null} - - ); -} +export const ConnectionCheck = ConnectVercelForm.ConnectionCheck; +export const Endpoint = ConnectVercelForm.Endpoint; diff --git a/frontend/src/app/forms/connect-vercel-form.tsx b/frontend/src/app/forms/connect-vercel-form.tsx index 981f12bcbf..9d8bc978c1 100644 --- a/frontend/src/app/forms/connect-vercel-form.tsx +++ b/frontend/src/app/forms/connect-vercel-form.tsx @@ -1,11 +1,12 @@ import { faCheck, faSpinnerThird, Icon } from "@rivet-gg/icons"; import { useQuery } from "@tanstack/react-query"; import { AnimatePresence, motion } from "framer-motion"; +import { useEffect, useState } from "react"; import { type UseFormReturn, useFormContext } from "react-hook-form"; import z from "zod"; import { + Button, CodeFrame, - CodeGroup, CodePreview, cn, createSchemaForm, @@ -16,13 +17,13 @@ import { FormLabel, FormMessage, Input, - ScrollArea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components"; +import { HelpDropdown } from "../help-dropdown"; export const formSchema = z.object({ plan: z.string(), @@ -116,7 +117,13 @@ export const Json = () => { ); }; -export const Endpoint = ({ className }: { className?: string }) => { +export const Endpoint = ({ + className, + placeholder, +}: { + className?: string; + placeholder?: string; +}) => { const { control } = useFormContext(); return ( { name="endpoint" render={({ field }) => ( - - Functions Endpoint - + Endpoint @@ -141,7 +149,7 @@ export const Endpoint = ({ className }: { className?: string }) => { }; export function ConnectionCheck() { - const { watch, formState } = useFormContext(); + const { watch } = useFormContext(); const endpoint = watch("endpoint"); const enabled = !!endpoint && z.string().url().safeParse(endpoint).success; @@ -180,7 +188,7 @@ export function ConnectionCheck() { success && "text-primary-foreground", )} initial={{ height: 0, opacity: 0.5 }} - animate={{ height: "4rem", opacity: 1 }} + animate={{ height: "6rem", opacity: 1 }} > {success ? ( <> @@ -191,16 +199,38 @@ export function ConnectionCheck() { Runner successfully connected ) : ( - <> - {" "} - Waiting for runner to connect... - +
+
+ {" "} + Waiting for Runner to connect... +
+ +
)} ) : null} ); } + +export function NeedHelp() { + const [open, setOpen] = useState(false); + + useEffect(() => { + const timeout = setTimeout(() => { + setOpen(true); + }, 10000); + return () => clearTimeout(timeout); + }, []); + + if (!open) return null; + + return ( + + + + ); +} diff --git a/frontend/src/components/copy-area.tsx b/frontend/src/components/copy-area.tsx index d0c8af7ffb..37b737af27 100644 --- a/frontend/src/components/copy-area.tsx +++ b/frontend/src/components/copy-area.tsx @@ -1,7 +1,7 @@ "use client"; import { Slot } from "@radix-ui/react-slot"; -import { faCopy, Icon } from "@rivet-gg/icons"; +import { faCopy, faEye, faEyeSlash, Icon } from "@rivet-gg/icons"; import { type ComponentProps, forwardRef, @@ -139,7 +139,7 @@ export const DiscreteCopyButton = forwardRef< type="button" variant="ghost" size={props.size} - className={cn(props.className, "max-w-full min-w-0")} + className={cn("max-w-full min-w-0", props.className)} endIcon={ ); } + +export function DiscreteInput({ + value, + show, +}: { + value: string; + show?: boolean; +}) { + const [showState, setShowState] = useState(!!show); + + const finalShow = showState || !!show; + return ( +
+ + + + {!show ? ( + + ) : null} +
+ ); +} diff --git a/frontend/src/components/ui/stepper.tsx b/frontend/src/components/ui/stepper.tsx index cbb5b23916..92127ec41b 100644 --- a/frontend/src/components/ui/stepper.tsx +++ b/frontend/src/components/ui/stepper.tsx @@ -228,7 +228,7 @@ const defineStepper = ( /> )} -
+
{panel}
diff --git a/frontend/src/routes/_context/_cloud.tsx b/frontend/src/routes/_context/_cloud.tsx index 3b8e38c3b5..95e6bb173b 100644 --- a/frontend/src/routes/_context/_cloud.tsx +++ b/frontend/src/routes/_context/_cloud.tsx @@ -77,6 +77,9 @@ function CloudModals() { }} />