diff --git a/frontend/src/app/dialogs/connect-aws-frame.tsx b/frontend/src/app/dialogs/connect-aws-frame.tsx index e63650839b..2515813963 100644 --- a/frontend/src/app/dialogs/connect-aws-frame.tsx +++ b/frontend/src/app/dialogs/connect-aws-frame.tsx @@ -1,6 +1,6 @@ import { faAws, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; -import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; +import ConnectManualServerlessFrameContent from "./connect-manual-serverless-frame"; interface ConnectAwsFrameContentProps extends DialogContentProps {} @@ -17,7 +17,7 @@ export default function ConnectAwsFrameContent({ - diff --git a/frontend/src/app/dialogs/connect-gcp-frame.tsx b/frontend/src/app/dialogs/connect-gcp-frame.tsx index 7ff9e811ac..70484f3035 100644 --- a/frontend/src/app/dialogs/connect-gcp-frame.tsx +++ b/frontend/src/app/dialogs/connect-gcp-frame.tsx @@ -1,6 +1,6 @@ import { faGoogleCloud, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; -import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; +import ConnectManualServerlessFrameContent from "./connect-manual-serverless-frame"; interface ConnectAwsFrameContentProps extends DialogContentProps {} @@ -18,7 +18,7 @@ export default function ConnectAwsFrameContent({ - diff --git a/frontend/src/app/dialogs/connect-hetzner-frame.tsx b/frontend/src/app/dialogs/connect-hetzner-frame.tsx index dd85f5292b..f32be3c48c 100644 --- a/frontend/src/app/dialogs/connect-hetzner-frame.tsx +++ b/frontend/src/app/dialogs/connect-hetzner-frame.tsx @@ -1,6 +1,6 @@ import { faHetznerH, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; -import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; +import ConnectManualServerlessFrameContent from "./connect-manual-serverless-frame"; interface ConnectHetznerFrameContentProps extends DialogContentProps {} @@ -18,7 +18,7 @@ export default function ConnectHetznerFrameContent({ - diff --git a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx index 1567ba94d6..a10d2fdd85 100644 --- a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx @@ -224,7 +224,7 @@ function Step2({ provider }: { provider: string }) { @@ -235,7 +235,7 @@ function Step3() { return ; } -export const useSelectedDatacenter = () => { +export const useEndpoint = () => { 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 858f4e7768..40312273d5 100644 --- a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx @@ -5,16 +5,23 @@ import { useSuspenseInfiniteQuery, } from "@tanstack/react-query"; import confetti from "canvas-confetti"; +import type { ComponentProps, ReactNode } from "react"; 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 { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + type DialogContentProps, +} from "@/components"; +import { 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 { useSelectedDatacenter } from "./connect-manual-serverfull-frame"; +import { useEndpoint } from "./connect-manual-serverfull-frame"; const stepper = defineStepper( { @@ -22,21 +29,7 @@ const stepper = defineStepper( title: "Configure", assist: false, next: "Next", - schema: z.object({ - runnerName: z.string().min(1, "Runner name is required"), - datacenters: z - .record(z.boolean()) - .refine( - (data) => Object.values(data).some(Boolean), - "At least one datacenter must be selected", - ), - headers: z.array(z.tuple([z.string(), z.string()])).default([]), - slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"), - maxRunners: z.coerce.number().min(0, "Must be 0 or greater"), - minRunners: z.coerce.number().min(0, "Must be 0 or greater"), - runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"), - requestLifespan: z.coerce.number().min(0, "Must be 0 or greater"), - }), + schema: ConnectServerlessForm.configurationSchema, }, { id: "step-2", @@ -49,23 +42,18 @@ const stepper = defineStepper( id: "step-3", title: "Confirm Connection", assist: false, - schema: z.object({ - endpoint: z - .string() - .nonempty("Endpoint is required") - .url("Please enter a valid URL"), - success: z.boolean().refine((v) => v === true, { - message: "Runner must be connected to proceed", - }), - }), + schema: ConnectServerlessForm.deploymentSchema, next: "Add", }, ); -interface ConnectManualServerlessFrameContentProps extends DialogContentProps {} +interface ConnectManualServerlessFrameContentProps extends DialogContentProps { + provider?: string; +} export default function ConnectManualServerlessFrameContent({ onClose, + provider, }: ConnectManualServerlessFrameContentProps) { usePrefetchInfiniteQuery({ ...useEngineCompatDataProvider().datacentersQueryOptions(), @@ -76,24 +64,28 @@ export default function ConnectManualServerlessFrameContent({ useEngineCompatDataProvider().datacentersQueryOptions(), ); - return ; + return ( + + ); } function FormStepper({ onClose, datacenters, + provider, }: { onClose?: () => void; - datacenters: Region[]; + datacenters: Rivet.Datacenter[]; + provider?: string; }) { - const provider = useEngineCompatDataProvider(); - - const { data } = useSuspenseInfiniteQuery({ - ...provider.runnerConfigsQueryOptions(), - }); + const dataProvider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...provider.upsertRunnerConfigMutationOptions(), + ...dataProvider.upsertRunnerConfigMutationOptions(), onSuccess: async () => { confetti({ angle: 60, @@ -107,7 +99,7 @@ function FormStepper({ }); await queryClient.invalidateQueries( - provider.runnerConfigsQueryOptions(), + dataProvider.runnerConfigsQueryOptions(), ); onClose?.(); }, @@ -116,44 +108,13 @@ function FormStepper({ { - let existing: Record = {}; - try { - const runnerConfig = await queryClient.fetchQuery( - provider.runnerConfigQueryOptions({ - name: values.runnerName, - }), - ); - existing = runnerConfig?.datacenters || {}; - } catch { - existing = {}; - } - - const selectedDatacenters = Object.entries(values.datacenters) - .filter(([, selected]) => selected) - .map(([id]) => id); - - const config = { - serverless: { - url: values.endpoint, - maxRunners: values.maxRunners, - slotsPerRunner: values.slotsPerRunner, - runnersMargin: values.runnerMargin, - requestLifespan: values.requestLifespan, - headers: Object.fromEntries( - values.headers.map(([key, value]) => [key, value]), - ), - }, - metadata: { - provider: "custom", + const payload = await buildServerlessConfig( + dataProvider, + values, + { + provider, }, - }; - - const payload = { - ...existing, - ...Object.fromEntries( - selectedDatacenters.map((dc) => [dc, config]), - ), - }; + ); await mutateAsync({ name: values.runnerName, @@ -176,23 +137,64 @@ function FormStepper({ content={{ "step-1": () => , "step-2": () => , - "step-3": () => , + "step-3": () => , }} /> ); } +export const buildServerlessConfig = async ( + dataProvider: ReturnType, + values: z.infer< + typeof ConnectServerlessForm.configurationSchema & + typeof ConnectServerlessForm.deploymentSchema + >, + { provider }: { provider?: string } = {}, +): Promise> => { + let existing: Record = {}; + try { + const runnerConfig = await queryClient.fetchQuery( + dataProvider.runnerConfigQueryOptions({ + name: values.runnerName, + }), + ); + existing = runnerConfig?.datacenters || {}; + } catch { + existing = {}; + } + + const selectedDatacenters = Object.entries(values.datacenters) + .filter(([, selected]) => selected) + .map(([id]) => id); + + const config = { + serverless: { + url: values.endpoint, + maxRunners: values.maxRunners, + slotsPerRunner: values.slotsPerRunner, + runnersMargin: values.runnerMargin, + requestLifespan: values.requestLifespan, + headers: Object.fromEntries( + values.headers.map(([key, value]) => [key, value]), + ), + }, + metadata: { + provider: provider || "custom", + }, + }; + + const payload = { + ...existing, + ...Object.fromEntries(selectedDatacenters.map((dc) => [dc, config])), + }; + + return payload; +}; + function Step1() { return (
- - - - - - - - +
); } @@ -203,18 +205,75 @@ function Step2() {

Set the following environment variables.

); } -function Step3() { +function Step3({ provider }: { provider?: string }) { + const providerDisplayName = provider + ? provider.charAt(0).toUpperCase() + provider.slice(1) + : undefined; return ( <> - + + + ); +} + +export function Configuration({ + runnerName = true, + datacenters = true, + headers = true, + slotsPerRunner = true, + minRunners = true, + maxRunners = true, + runnerMargin = true, + requestLifespan = true, +}: { + runnerName?: boolean; + datacenters?: boolean; + headers?: boolean; + slotsPerRunner?: boolean; + minRunners?: boolean; + maxRunners?: boolean; + runnerMargin?: boolean; + requestLifespan?: boolean; +}) { + return ( + <> + {runnerName && } + {datacenters && } + {headers && } + {slotsPerRunner && } + {minRunners && } + {maxRunners && } + {runnerMargin && } + {requestLifespan && } ); } + +export function ConfigurationAccordion({ + prefixFields, + ...props +}: ComponentProps & { prefixFields?: ReactNode }) { + return ( + + + + Advanced + + + {prefixFields} + + + + + ); +} diff --git a/frontend/src/app/dialogs/connect-quick-railway-frame.tsx b/frontend/src/app/dialogs/connect-quick-railway-frame.tsx index 4267a1cf9d..2d5bbadbab 100644 --- a/frontend/src/app/dialogs/connect-quick-railway-frame.tsx +++ b/frontend/src/app/dialogs/connect-quick-railway-frame.tsx @@ -5,44 +5,37 @@ import { useSuspenseInfiniteQuery, } from "@tanstack/react-query"; import confetti from "canvas-confetti"; -import z from "zod"; +import { useWatch } from "react-hook-form"; import * as ConnectRailwayForm from "@/app/forms/connect-railway-form"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - type DialogContentProps, - ExternalLinkCard, - Frame, -} from "@/components"; +import { type DialogContentProps, ExternalLinkCard, Frame } from "@/components"; import { useEngineCompatDataProvider } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; import { queryClient } from "@/queries/global"; import { useRailwayTemplateLink } from "@/utils/use-railway-template-link"; +import { + configurationSchema, + deploymentSchema, +} from "../forms/connect-manual-serverless-form"; import { StepperForm } from "../forms/stepper-form"; +import { + buildServerlessConfig, + ConfigurationAccordion, +} from "./connect-manual-serverless-frame"; const stepper = defineStepper( { id: "step-1", - title: "Deploy to Railway", - assist: false, + title: "Configure", + assist: true, next: "Next", - schema: z.object({ - runnerName: z.string().min(1, "Runner name is required"), - datacenter: z.string().min(1, "Please select a region"), - }), + schema: configurationSchema, }, { - id: "step-2", - title: "Wait for the Runner to connect", + id: "deploy", + title: "Configure Railway endpoint", assist: true, - schema: z.object({ - success: z.boolean().refine((v) => v === true, { - message: "Runner must be connected to proceed", - }), - }), - next: "Add", + next: "Done", + schema: deploymentSchema, }, ); @@ -55,18 +48,6 @@ export default function ConnectQuickRailwayFrameContent({ ...useEngineCompatDataProvider().datacentersQueryOptions(), pages: Infinity, }); - const { data } = useSuspenseInfiniteQuery( - useEngineCompatDataProvider().datacentersQueryOptions(), - ); - - const prefferedRegionForRailway = - data.find((region) => region.name.toLowerCase().includes("us-west")) - ?.name || - data.find((region) => region.name.toLowerCase().includes("us-east")) - ?.name || - data.find((region) => region.name.toLowerCase().includes("ore")) - ?.name || - "auto"; return ( <> @@ -78,23 +59,17 @@ export default function ConnectQuickRailwayFrameContent({ - + ); } -function FormStepper({ - onClose, - defaultDatacenter, -}: { - onClose?: () => void; - defaultDatacenter: string; -}) { +function FormStepper({ onClose }: { onClose?: () => void }) { const provider = useEngineCompatDataProvider(); + const { data: datacenters } = useSuspenseInfiniteQuery( + useEngineCompatDataProvider().datacentersQueryOptions(), + ); const { mutateAsync } = useMutation({ ...provider.upsertRunnerConfigMutationOptions(), onSuccess: async () => { @@ -118,57 +93,81 @@ function FormStepper({ return ( { + const payload = await buildServerlessConfig(provider, values, { + provider: "railway", + }); await mutateAsync({ name: values.runnerName, - config: { - [values.datacenter]: { - normal: {}, - metadata: { provider: "railway" }, - }, - }, + config: payload, }); }} defaultValues={{ runnerName: "default", - success: true, - datacenter: defaultDatacenter, + slotsPerRunner: 1, + minRunners: 1, + maxRunners: 10_000, + runnerMargin: 0, + requestLifespan: 55, + headers: [], + success: false, + datacenters: Object.fromEntries( + datacenters.map((dc) => [dc.name, true]), + ), }} content={{ - "step-1": () => , - "step-2": () => , + "step-1": () => , + deploy: () => , }} /> ); } -function Step1({ datacenter }: { datacenter: string }) { +function Step1() { return ( <>

Deploy the Rivet Railway template to get started quickly.

- +
- - - - Advanced Options - - - - - - - ); } -function DeployToRailwayButton({ datacenter }: { datacenter: string }) { - const runnerName = "default"; +function DeployStep() { + return ( + <> +

Paste your deployment's endpoint below:

+
+ + + +

+ Need help deploying? See{" "} + + Railway's deployment documentation + + . +

+
+ + + ); +} + +function DeployToRailwayButton() { + const runnerName = useWatch({ name: "runnerName" }); const url = useRailwayTemplateLink({ runnerName, - datacenter, + kind: "serverless", }); return ( @@ -179,7 +178,3 @@ function DeployToRailwayButton({ datacenter }: { datacenter: string }) { /> ); } - -function Step2() { - return ; -} diff --git a/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx b/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx index cd3853139a..02faa5595a 100644 --- a/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx +++ b/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx @@ -1,27 +1,23 @@ import { faVercel, Icon } from "@rivet-gg/icons"; +import type { Rivet } from "@rivetkit/engine-api-full"; import { useMutation, usePrefetchInfiniteQuery, - useQuery, useSuspenseInfiniteQuery, } from "@tanstack/react-query"; import confetti from "canvas-confetti"; import { useMemo } from "react"; import * as ConnectVercelForm from "@/app/forms/connect-quick-vercel-form"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - type DialogContentProps, - ExternalLinkCard, - Frame, -} from "@/components"; -import { type Region, useEngineCompatDataProvider } from "@/components/actors"; -import { usePublishableToken } from "@/queries/accessors"; +import { type DialogContentProps, ExternalLinkCard, Frame } from "@/components"; +import { useEngineCompatDataProvider } from "@/components/actors"; import { queryClient } from "@/queries/global"; +import { useRivetDsn } from "../env-variables"; import { StepperForm } from "../forms/stepper-form"; -import { useSelectedDatacenter } from "./connect-manual-serverfull-frame"; +import { useEndpoint } from "./connect-manual-serverfull-frame"; +import { + buildServerlessConfig, + ConfigurationAccordion, +} from "./connect-manual-serverless-frame"; import { VERCEL_SERVERLESS_MAX_DURATION } from "./connect-vercel-frame"; const { stepper } = ConnectVercelForm; @@ -62,7 +58,7 @@ function FormStepper({ onClose, }: { onClose?: () => void; - datacenters: Region[]; + datacenters: Rivet.Datacenter[]; }) { const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ @@ -95,28 +91,13 @@ function FormStepper({ deploy: () => , }} onSubmit={async ({ values }) => { - const selectedDatacenters = Object.entries(values.datacenters) - .filter(([, selected]) => selected) - .map(([id]) => id); - - const config = { - serverless: { - url: values.endpoint, - maxRunners: values.maxRunners, - slotsPerRunner: values.slotsPerRunner, - runnersMargin: values.runnerMargin, - requestLifespan: VERCEL_SERVERLESS_MAX_DURATION - 5, // Subtract 5s to ensure we don't hit Vercel's timeout - headers: Object.fromEntries( - values.headers.map(([key, value]) => [key, value]), - ), - }, - metadata: { - provider: "vercel", + const payload = await buildServerlessConfig( + provider, + { + ...values, + requestLifespan: VERCEL_SERVERLESS_MAX_DURATION - 5, }, - }; - - const payload = Object.fromEntries( - selectedDatacenters.map((dc) => [dc, config]), + { provider: "vercel" }, ); await mutateAsync({ @@ -132,6 +113,7 @@ function FormStepper({ runnerMargin: 0, headers: [], success: false, + plan: "hobby", datacenters: Object.fromEntries( datacenters.map((dc) => [dc.name, true]), ), @@ -141,28 +123,21 @@ function FormStepper({ } const useVercelTemplateLink = () => { - const dataProvider = useEngineCompatDataProvider(); - const token = usePublishableToken(); - const endpoint = useSelectedDatacenter(); + const endpoint = useEndpoint(); + + const dsn = useRivetDsn({ endpoint, kind: "serverless" }); return useMemo(() => { const repositoryUrl = "https://github.com/rivet-dev/template-vercel"; - const env = [ - "RIVET_ENDPOINT", - "NEXT_PUBLIC_RIVET_ENDPOINT", - "NEXT_PUBLIC_RIVET_TOKEN", - "NEXT_PUBLIC_RIVET_NAMESPACE", - ].join(","); + const env = ["RIVET_ENDPOINT", "NEXT_PUBLIC_RIVET_ENDPOINT"].join(","); const projectName = "rivetkit-vercel"; const envDefaults = { - RIVET_ENDPOINT: endpoint, - NEXT_PUBLIC_RIVET_ENDPOINT: endpoint, - NEXT_PUBLIC_RIVET_TOKEN: token, - NEXT_PUBLIC_RIVET_NAMESPACE: dataProvider.engineNamespace, + RIVET_ENDPOINT: dsn, + NEXT_PUBLIC_RIVET_ENDPOINT: dsn, }; return `https://vercel.com/new/clone?repository-url=${encodeURIComponent(repositoryUrl)}&env=${env}&project-name=${projectName}&repository-name=${projectName}&envDefaults=${encodeURIComponent(JSON.stringify(envDefaults))}`; - }, [dataProvider.engineNamespace, endpoint, token]); + }, [dsn]); }; function StepInitialInfo() { @@ -188,25 +163,29 @@ function StepInitialInfo() { function StepDeploy() { return ( <> +

+ Deploy your code to Vercel and paste your deployment's endpoint: +

- - - - Advanced - - - - - - - - - - - - + } + /> +

+ Need help deploying? See{" "} + + Vercel's deployment documentation + + . +

+ ); diff --git a/frontend/src/app/dialogs/connect-railway-frame.tsx b/frontend/src/app/dialogs/connect-railway-frame.tsx index fa5fe7710f..0cbe085809 100644 --- a/frontend/src/app/dialogs/connect-railway-frame.tsx +++ b/frontend/src/app/dialogs/connect-railway-frame.tsx @@ -2,29 +2,27 @@ import { faRailway, Icon } from "@rivet-gg/icons"; import { useMutation, usePrefetchInfiniteQuery, - useQuery, useSuspenseInfiniteQuery, } from "@tanstack/react-query"; import confetti from "canvas-confetti"; import { useWatch } from "react-hook-form"; import z from "zod"; import * as ConnectRailwayForm from "@/app/forms/connect-railway-form"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - type DialogContentProps, - Frame, -} from "@/components"; +import { type DialogContentProps, Frame } 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 { + configurationSchema, + deploymentSchema, +} from "../forms/connect-manual-serverless-form"; import { StepperForm } from "../forms/stepper-form"; -import { useSelectedDatacenter } from "./connect-manual-serverfull-frame"; +import { useEndpoint } from "./connect-manual-serverfull-frame"; +import { + buildServerlessConfig, + ConfigurationAccordion, +} from "./connect-manual-serverless-frame"; const stepper = defineStepper( { @@ -32,28 +30,17 @@ const stepper = defineStepper( title: "Configure", assist: false, next: "Next", - schema: z.object({ - runnerName: z.string().min(1, "Runner name is required"), - datacenter: z.string().min(1, "Please select a region"), - }), + schema: z.object({}), }, { id: "step-2", title: "Deploy to Railway", assist: false, - schema: z.object({}), - next: "Next", - }, - { - id: "step-3", - title: "Wait for the Runner to connect", - assist: true, schema: z.object({ - success: z.boolean().refine((v) => v === true, { - message: "Runner must be connected to proceed", - }), + ...configurationSchema.shape, + ...deploymentSchema.shape, }), - next: "Add", + next: "Done", }, ); @@ -66,18 +53,6 @@ export default function ConnectRailwayFrameContent({ ...useEngineCompatDataProvider().datacentersQueryOptions(), pages: Infinity, }); - const { data } = useSuspenseInfiniteQuery( - useEngineCompatDataProvider().datacentersQueryOptions(), - ); - - const prefferedRegionForRailway = - data.find((region) => region.name.toLowerCase().includes("us-west")) - ?.name || - data.find((region) => region.name.toLowerCase().includes("us-east")) - ?.name || - data.find((region) => region.name.toLowerCase().includes("ore")) - ?.name || - "auto"; return ( <> @@ -89,22 +64,21 @@ export default function ConnectRailwayFrameContent({ - + ); } -function FormStepper({ - onClose, - defaultDatacenter, -}: { - onClose?: () => void; - defaultDatacenter: string; -}) { +function FormStepper({ onClose }: { onClose?: () => void }) { + usePrefetchInfiniteQuery({ + ...useEngineCompatDataProvider().datacentersQueryOptions(), + pages: Infinity, + }); + + const { data: datacenters } = useSuspenseInfiniteQuery( + useEngineCompatDataProvider().datacentersQueryOptions(), + ); const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ ...provider.upsertRunnerConfigMutationOptions(), @@ -130,25 +104,31 @@ function FormStepper({ { + const payload = await buildServerlessConfig(provider, values, { + provider: "railway", + }); + await mutateAsync({ name: values.runnerName, - config: { - [values.datacenter]: { - normal: {}, - metadata: { provider: "railway" }, - }, - }, + config: payload, }); }} defaultValues={{ runnerName: "default", - success: true, - datacenter: defaultDatacenter, + slotsPerRunner: 1, + minRunners: 1, + maxRunners: 10_000, + runnerMargin: 0, + requestLifespan: 55, + headers: [], + success: false, + datacenters: Object.fromEntries( + datacenters.map((dc) => [dc.name, true]), + ), }} content={{ "step-1": () => , - "step-2": () => , - "step-3": () => , + "step-2": () => , }} /> ); @@ -157,68 +137,57 @@ function FormStepper({ function Step1() { return ( <> -
- We're going to help you deploy a RivetKit project to Railway and - connect it to Rivet. -
- - - - Advanced - - - - - - - - - ); -} - -function Step2() { - return ( - <> -

Deploy any RivetKit app to Railway.

-

Or use our Railway template to get started quickly.

-

- Set the following environment variables in your Railway project - settings. + If you have not deployed a project, see the{" "} + + Railway quickstart guide + + . +

+

+ Set these variables in Settings > Variables in the Railway + dashboard.

); } -function Step3() { - return ; -} - -function DeployToRailwayButton() { - const runnerName = useWatch({ name: "runnerName" }); - - const url = useRailwayTemplateLink({ - runnerName: runnerName || "default", - datacenter: useWatch({ name: "datacenter" }) || "auto", - }); - +function StepDeploy() { return ( - - Deploy to Railway - + <> +

+ Deploy your code to Railway and paste your deployment's + endpoint: +

+
+ + +

+ Need help deploying? See{" "} + + Railway's deployment documentation + + . +

+
+ + ); } diff --git a/frontend/src/app/dialogs/connect-vercel-frame.tsx b/frontend/src/app/dialogs/connect-vercel-frame.tsx index 582f25afa1..25d0d3a38e 100644 --- a/frontend/src/app/dialogs/connect-vercel-frame.tsx +++ b/frontend/src/app/dialogs/connect-vercel-frame.tsx @@ -1,51 +1,25 @@ import { faVercel, Icon } from "@rivet-gg/icons"; +import type { Rivet } from "@rivetkit/engine-api-full"; import { useMutation, usePrefetchInfiniteQuery, useSuspenseInfiniteQuery, } from "@tanstack/react-query"; -import { useRouteContext } from "@tanstack/react-router"; import confetti from "canvas-confetti"; import { useWatch } from "react-hook-form"; -import { match } from "ts-pattern"; -import type z from "zod"; import * as ConnectVercelForm from "@/app/forms/connect-vercel-form"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - type DialogContentProps, - Frame, - getConfig, -} from "@/components"; +import { type DialogContentProps, Frame } from "@/components"; import { useEngineCompatDataProvider } from "@/components/actors"; -import { cloudEnv } from "@/lib/env"; -import { usePublishableToken } from "@/queries/accessors"; import { queryClient } from "@/queries/global"; -import { type JoinStepSchemas, StepperForm } from "../forms/stepper-form"; +import { StepperForm } from "../forms/stepper-form"; +import { buildServerlessConfig, ConfigurationAccordion } from "./connect-manual-serverless-frame"; const { stepper } = ConnectVercelForm; -type FormValues = z.infer>; - export const VERCEL_SERVERLESS_MAX_DURATION = 300; interface CreateProjectFrameContentProps extends DialogContentProps {} -const useEndpoint = () => { - return match(__APP_TYPE__) - .with("cloud", () => { - return cloudEnv().VITE_APP_API_URL; - }) - .with("engine", () => { - return getConfig().apiUrl; - }) - .otherwise(() => { - throw new Error("Not in a valid context"); - }); -}; - export default function CreateProjectFrameContent({ onClose, }: CreateProjectFrameContentProps) { @@ -80,12 +54,9 @@ function FormStepper({ onClose, }: { onClose?: () => void; - datacenters: Region[]; + datacenters: Rivet.Datacenter[]; }) { const provider = useEngineCompatDataProvider(); - const token = usePublishableToken(); - const endpoint = useEndpoint(); - const namespace = provider.engineNamespace; const { mutateAsync } = useMutation({ ...provider.upsertRunnerConfigMutationOptions(), @@ -110,15 +81,8 @@ function FormStepper({ , "api-route": () => , - frontend: () => ( - - ), + frontend: () => , variables: () => ( <>

@@ -131,29 +95,15 @@ function FormStepper({ deploy: () => , }} onSubmit={async ({ values }) => { - const selectedDatacenters = Object.entries(values.datacenters) - .filter(([, selected]) => selected) - .map(([id]) => id); - - const config = { - serverless: { - url: values.endpoint, - maxRunners: values.maxRunners, - slotsPerRunner: values.slotsPerRunner, - runnersMargin: values.runnerMargin, - requestLifespan: VERCEL_SERVERLESS_MAX_DURATION - 5, // Subtract 5s to ensure we don't hit Vercel's timeout - headers: Object.fromEntries( - values.headers.map(([key, value]) => [key, value]), - ), - }, - metadata: { - provider: "vercel", - }, - }; - - const payload = Object.fromEntries( - selectedDatacenters.map((dc) => [dc, config]), - ); + const payload = + await buildServerlessConfig( + provider, + { + ...values, + requestLifespan: VERCEL_SERVERLESS_MAX_DURATION - 5, + }, + { provider: "vercel" }, + ); await mutateAsync({ name: values.runnerName, @@ -177,51 +127,13 @@ function FormStepper({ ); } -// function StepInitialInfo() { -// return ( -// <> -// -// -// -// -// Advanced -// -// -// -// -// -// -// -// -// -// -// -// -// -// ); -// } - function StepApiRoute() { const plan = useWatch({ name: "plan" }); return ; } -function StepFrontend({ - token, - endpoint, - namespace, -}: { - token: string; - endpoint: string; - namespace: string; -}) { - return ( - - ); +function StepFrontend() { + return ; } function StepDeploy() { @@ -232,36 +144,25 @@ function StepDeploy() {

- - - - Advanced - - - - - - - - - - - - + } + /> +

+ Need help deploying? See{" "} + + Vercel's deployment documentation + + . +

+ -

- Need help deploying? See{" "} - - Vercel's deployment documentation - - . -

); } diff --git a/frontend/src/app/env-variables.tsx b/frontend/src/app/env-variables.tsx index 7d0bdbdf84..72d17b99e2 100644 --- a/frontend/src/app/env-variables.tsx +++ b/frontend/src/app/env-variables.tsx @@ -30,11 +30,13 @@ export function EnvVariables({ {prefixlessEndpoint ? ( - + ) : null} - - - +
@@ -90,43 +92,35 @@ function RivetRunnerEnv({ ); } -function RivetTokenEnv({ - prefix, +export const useRivetDsn = ({ + endpoint, kind, }: { - prefix?: string; + endpoint: string; kind: "serverless" | "serverfull"; -}) { +}) => { + const dataProvider = useEngineCompatDataProvider(); const publishableToken = usePublishableToken(); const adminToken = useAdminToken(); + const token = kind === "serverless" ? publishableToken : adminToken; - return ( - <> - + const dsn = `https://${token}:${dataProvider.engineNamespace}@${endpoint + .replace("https://", "") + .replace("http://", "")}`; - - - ); -} + return dsn; +}; function RivetEndpointEnv({ prefix, endpoint, + kind, }: { prefix?: string; endpoint: string; + kind: "serverless" | "serverfull"; }) { + const dsn = useRivetDsn({ endpoint, kind }); return ( <> - - ); -} - -function RivetNamespaceEnv({ prefix }: { prefix?: string }) { - const dataProvider = useEngineCompatDataProvider(); - return ( - <> - - diff --git a/frontend/src/app/forms/connect-manual-serverless-form.tsx b/frontend/src/app/forms/connect-manual-serverless-form.tsx index 6b308c4217..f44e99eddb 100644 --- a/frontend/src/app/forms/connect-manual-serverless-form.tsx +++ b/frontend/src/app/forms/connect-manual-serverless-form.tsx @@ -36,11 +36,31 @@ import { ActorRegion, useEngineCompatDataProvider } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; import { VisibilitySensor } from "@/components/visibility-sensor"; -const endpointSchema = z +export const endpointSchema = z .string() .nonempty("Endpoint is required") - .url("Please enter a valid URL") - .endsWith("/api/rivet", "Endpoint must end with /api/rivet"); + .url("Please enter a valid URL"); + +export const configurationSchema = z.object({ + runnerName: z.string().min(1, "Runner name is required"), + datacenters: z + .record(z.boolean()) + .refine( + (data) => Object.values(data).some(Boolean), + "At least one datacenter must be selected", + ), + headers: z.array(z.tuple([z.string(), z.string()])).default([]), + slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"), + maxRunners: z.coerce.number().min(1, "Must be at least 1"), + minRunners: z.coerce.number().min(0, "Must be 0 or greater"), + runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"), + requestLifespan: z.coerce.number().min(0, "Must be 0 or greater"), +}); + +export const deploymentSchema = z.object({ + success: z.boolean().refine((val) => val, "Connection failed"), + endpoint: endpointSchema, +}); export const stepper = defineStepper( { @@ -48,21 +68,7 @@ export const stepper = defineStepper( title: "Configure", assist: false, next: "Next", - schema: z.object({ - plan: z.string().min(1, "Please select a Vercel plan"), - runnerName: z.string().min(1, "Runner name is required"), - datacenters: z - .record(z.boolean()) - .refine( - (data) => Object.values(data).some(Boolean), - "At least one datacenter must be selected", - ), - headers: z.array(z.tuple([z.string(), z.string()])).default([]), - slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"), - maxRunners: z.coerce.number().min(1, "Must be at least 1"), - minRunners: z.coerce.number().min(0, "Must be 0 or greater"), - runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"), - }), + schema: configurationSchema, }, { id: "step-2", @@ -76,10 +82,7 @@ export const stepper = defineStepper( title: "Deploy to Vercel", assist: true, next: "Done", - schema: z.object({ - success: z.boolean().refine((val) => val, "Connection failed"), - endpoint: endpointSchema, - }), + schema: deploymentSchema, }, ); @@ -163,6 +166,7 @@ export const MinRunners = ({ className }: { className?: string }) => { type="number" {...field} value={field.value || ""} + min={0} /> @@ -189,6 +193,7 @@ export const MaxRunners = ({ className }: { className?: string }) => { type="number" {...field} value={field.value || ""} + min={0} /> @@ -218,6 +223,7 @@ export const SlotsPerRunner = ({ className }: { className?: string }) => { type="number" {...field} value={field.value || ""} + min={0} /> @@ -240,7 +246,12 @@ export const RunnerMargin = ({ className }: { className?: string }) => { Runner Margin - + The number of extra runners to keep running to handle @@ -265,7 +276,12 @@ export const RequestLifespan = ({ className }: { className?: string }) => { Request Lifespan (seconds) - + The maximum duration (in seconds) that a request can run @@ -472,12 +488,13 @@ export function ConnectionCheck({ provider }: { provider: string }) { {isSuccess ? ( <> @@ -504,17 +521,6 @@ export function ConnectionCheck({ provider }: { provider: string }) { provider={provider} /> ) : null} -

- Endpoint{" "} - - {endpoint} - -

) : (
diff --git a/frontend/src/app/forms/connect-quick-vercel-form.tsx b/frontend/src/app/forms/connect-quick-vercel-form.tsx index e078ec63ad..78f00d043a 100644 --- a/frontend/src/app/forms/connect-quick-vercel-form.tsx +++ b/frontend/src/app/forms/connect-quick-vercel-form.tsx @@ -1,13 +1,10 @@ import z from "zod"; 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() - .nonempty("Endpoint is required") - .url("Please enter a valid URL") - .endsWith("/api/rivet", "Endpoint must end with /api/rivet"); +import { + configurationSchema, + deploymentSchema, +} from "./connect-manual-serverless-form"; export const stepper = defineStepper( { @@ -16,18 +13,8 @@ export const stepper = defineStepper( assist: false, next: "Next", schema: z.object({ - runnerName: z.string().min(1, "Runner name is required"), - datacenters: z - .record(z.boolean()) - .refine( - (data) => Object.values(data).some(Boolean), - "At least one datacenter must be selected", - ), - headers: z.array(z.tuple([z.string(), z.string()])).default([]), - slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"), - maxRunners: z.coerce.number().min(1, "Must be at least 1"), - minRunners: z.coerce.number().min(0, "Must be 0 or greater"), - runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"), + ...configurationSchema.omit({ requestLifespan: true }).shape, + plan: z.string().min(1, "Please select a Vercel plan"), }), }, { @@ -35,10 +22,7 @@ export const stepper = defineStepper( title: "Configure Vercel endpoint", assist: true, next: "Done", - schema: z.object({ - success: z.boolean().refine((val) => val, "Connection failed"), - endpoint: endpointSchema, - }), + schema: deploymentSchema, }, ); @@ -60,4 +44,6 @@ export const Endpoint = ConnectVercelForm.Endpoint; export const ConnectionCheck = ConnectVercelForm.ConnectionCheck; +export const Plan = ConnectVercelForm.Plan; + export const EnvVariables = ConnectVercelForm.EnvVariables; diff --git a/frontend/src/app/forms/connect-railway-form.tsx b/frontend/src/app/forms/connect-railway-form.tsx index ab34c2c7ae..cd85db39bb 100644 --- a/frontend/src/app/forms/connect-railway-form.tsx +++ b/frontend/src/app/forms/connect-railway-form.tsx @@ -1,17 +1,7 @@ -import * as ConnectManualServerlfullForm from "@/app/forms/connect-manual-serverfull-form"; +import * as ConnectManualServerlessForm from "@/app/forms/connect-manual-serverless-form"; -export const RunnerName = ConnectManualServerlfullForm.RunnerName; -export const Datacenter = () => { - return ( - - You can find the region your Railway runners are running in - under Settings > Deploy - - } - /> - ); -}; +export const RunnerName = ConnectManualServerlessForm.RunnerName; +export const Datacenters = ConnectManualServerlessForm.Datacenters; -export const ConnectionCheck = ConnectManualServerlfullForm.ConnectionCheck; +export const ConnectionCheck = ConnectManualServerlessForm.ConnectionCheck; +export const Endpoint = ConnectManualServerlessForm.Endpoint; diff --git a/frontend/src/app/forms/connect-vercel-form.tsx b/frontend/src/app/forms/connect-vercel-form.tsx index b2d503379f..a5374fee10 100644 --- a/frontend/src/app/forms/connect-vercel-form.tsx +++ b/frontend/src/app/forms/connect-vercel-form.tsx @@ -18,37 +18,13 @@ 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() - .nonempty("Endpoint is required") - .url("Please enter a valid URL") - .endsWith("/api/rivet", "Endpoint must end with /api/rivet"); +import { useEndpoint } from "../dialogs/connect-manual-serverfull-frame"; +import { + EnvVariables as EnvVariablesSection, + useRivetDsn, +} from "../env-variables"; export const stepper = defineStepper( - // { - // id: "initial-info", - // title: "Configure", - // assist: false, - // next: "Next", - // schema: z.object({ - // plan: z.string().min(1, "Please select a Vercel plan"), - // runnerName: z.string().min(1, "Runner name is required"), - // datacenters: z - // .record(z.boolean()) - // .refine( - // (data) => Object.values(data).some(Boolean), - // "At least one datacenter must be selected", - // ), - // headers: z.array(z.tuple([z.string(), z.string()])).default([]), - // slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"), - // maxRunners: z.coerce.number().min(1, "Must be at least 1"), - // minRunners: z.coerce.number().min(0, "Must be 0 or greater"), - // runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"), - // }), - // }, { id: "api-route", title: "Add API Route", @@ -78,20 +54,10 @@ export const stepper = defineStepper( assist: true, next: "Done", schema: z.object({ - success: z.boolean().refine((val) => val, "Connection failed"), - endpoint: endpointSchema, - runnerName: z.string().min(1, "Runner name is required"), - datacenters: z - .record(z.boolean()) - .refine( - (data) => Object.values(data).some(Boolean), - "At least one datacenter must be selected", - ), - headers: z.array(z.tuple([z.string(), z.string()])).default([]), - slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"), - maxRunners: z.coerce.number().min(1, "Must be at least 1"), - minRunners: z.coerce.number().min(0, "Must be 0 or greater"), - runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"), + ...ConnectManualServerlessForm.deploymentSchema.shape, + ...ConnectManualServerlessForm.configurationSchema.omit({ + requestLifespan: true, + }).shape, plan: z.string().min(1, "Please select a Vercel plan"), }), }, @@ -148,20 +114,6 @@ export const RunnerMargin = ConnectManualServerlessForm.RunnerMargin; export const Headers = ConnectManualServerlessForm.Headers; -// export const PLAN_TO_MAX_DURATION: Record = { -// hobby: 300, -// pro: 800, -// enterprise: 800, -// }; -// -// const integrationCode = ({ plan }: { plan: string }) => -// `import { toNextHandler } from "@rivetkit/next-js"; -// import { registry } from "@/rivet/registry"; -// -// export const maxDuration = ${PLAN_TO_MAX_DURATION[plan] || 60}; // [!code highlight] -// -// export const { GET, POST, PUT, PATCH, HEAD, OPTIONS } = toNextHandler(registry);`; - const integrationCode = ({ plan }: { plan: string }) => `import { toNextHandler } from "@rivetkit/next-js"; import { registry } from "@/rivet/registry"; @@ -257,23 +209,17 @@ export const Endpoint = ConnectManualServerlessForm.Endpoint; export const ConnectionCheck = ConnectManualServerlessForm.ConnectionCheck; -export const FrontendIntegrationCode = ({ - token, - endpoint, - namespace, -}: { - token: string; - endpoint: string; - namespace: string; -}) => { +export const FrontendIntegrationCode = () => { + const endpoint = useRivetDsn({ + endpoint: useEndpoint(), + kind: "serverless", + }); const clientCode = `"use client"; import { createRivetKit } from "@rivetkit/next-js/client"; import type { registry } from "@/rivet/registry"; export const { useActor } = createRivetKit({ endpoint: "${endpoint}", - namespace: "${namespace}", - token: "${token}", }); `; @@ -302,10 +248,10 @@ export function EnvVariables() { return ( ); -} +} \ No newline at end of file diff --git a/frontend/src/app/forms/stepper-form.tsx b/frontend/src/app/forms/stepper-form.tsx index 1b83b58bac..cd61f1b04b 100644 --- a/frontend/src/app/forms/stepper-form.tsx +++ b/frontend/src/app/forms/stepper-form.tsx @@ -170,7 +170,7 @@ function StepPanel({ stepper, step, content, - showPrevious, + showPrevious = true, showControls = true, }: Pick, "Stepper" | "content"> & { stepper: Stepperize.Stepper; @@ -186,7 +186,7 @@ function StepPanel({ {showControls ? ( {step.assist ? : null} - {showPrevious ? ( + {showPrevious && !stepper.isFirst ? (
+ ); + } + if (metadata.provider === "aws") { + return ( +
+ AWS ECS
); } if (metadata.provider === "gcp") { return (
- Google Cloud Run + Google Cloud + Run
); } diff --git a/frontend/src/utils/use-railway-template-link.ts b/frontend/src/utils/use-railway-template-link.ts index c652b5dc4a..a5ec14c38e 100644 --- a/frontend/src/utils/use-railway-template-link.ts +++ b/frontend/src/utils/use-railway-template-link.ts @@ -1,30 +1,31 @@ -import { useQuery } from "@tanstack/react-query"; -import { useEngineCompatDataProvider } from "@/components/actors"; -import { engineEnv } from "@/lib/env"; +import { useMemo } from "react"; +import { useEndpoint } from "@/app/dialogs/connect-manual-serverfull-frame"; +import { useRivetDsn } from "@/app/env-variables"; export function useRailwayTemplateLink({ runnerName, - datacenter, + kind, }: { runnerName: string; - datacenter: string; + kind: "serverless" | "serverfull"; }) { - const dataProvider = useEngineCompatDataProvider(); - const { data: token } = useQuery( - dataProvider.engineAdminTokenQueryOptions(), - ); - const endpoint = useDatacenterEndpoint({ datacenter }); + const endpoint = useEndpoint(); + const dsn = useRivetDsn({ endpoint, kind }); - return `https://railway.com/new/template/rivet-cloud-starter?referralCode=RC7bza&utm_medium=integration&utm_source=template&utm_campaign=generic&RIVET_TOKEN=${token || ""}&RIVET_ENDPOINT=${ - endpoint || "" - }&RIVET_NAMESPACE=${ - dataProvider.engineNamespace || "" - }&RIVET_RUNNER=${runnerName || ""}`; -} + return useMemo(() => { + const url = new URL( + "https://railway.com/new/template/rivet-cloud-starter", + ); + url.searchParams.set("referralCode", "RC7bza"); + url.searchParams.set("utm_medium", "integration"); + url.searchParams.set("utm_source", "template"); + url.searchParams.set("utm_campaign", "generic"); + + url.searchParams.set("RIVET_RUNNER", runnerName || ""); + if (dsn) { + url.searchParams.set("RIVET_ENDPOINT", dsn); + } -const useDatacenterEndpoint = ({ datacenter }: { datacenter: string }) => { - const { data } = useQuery( - useEngineCompatDataProvider().datacenterQueryOptions(datacenter), - ); - return data?.url || engineEnv().VITE_APP_API_URL; -}; + return url.toString(); + }, [runnerName, dsn]); +}