diff --git a/frontend/package.json b/frontend/package.json index 633ef73254..b0cd5ec3f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,6 +60,7 @@ "@rivet-gg/cloud": "https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c", "@rivet-gg/icons": "workspace:*", "@rivetkit/engine-api-full": "workspace:*", + "@rivetkit/example-registry": "workspace:^", "@sentry/react": "^8.55.0", "@sentry/vite-plugin": "^2.23.1", "@shikijs/langs": "^3.12.2", @@ -129,6 +130,7 @@ "ts-pattern": "^5.8.0", "typescript": "^5.9.2", "typescript-plugin-css-modules": "^5.2.0", + "unplugin-macros": "^0.18.3", "usehooks-ts": "^3.1.1", "vite": "^5.4.20", "vite-plugin-favicons-inject": "^2.2.0", diff --git a/frontend/packages/components/src/actors/get-started.tsx b/frontend/packages/components/src/actors/get-started.tsx deleted file mode 100644 index 8039cb19f9..0000000000 --- a/frontend/packages/components/src/actors/get-started.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Icon, faActors, faFunction, faServer } from "@rivet-gg/icons"; -import { motion } from "framer-motion"; -import type { ComponentProps } from "react"; -import { DocsSheet } from "../docs-sheet"; -import { cn } from "../lib/utils"; -import { Button } from "../ui/button"; - -export function ActorsResources() { - return ( - <> -
- - - -
- - ); -} - -const linkVariants = { - hidden: { - opacity: 0, - }, - show: { - opacity: 1, - }, -}; - -interface ExampleLinkProps { - title: string; - description?: string; - icon: ComponentProps["icon"]; - href: string; - size?: "sm" | "md" | "lg"; -} - -function ExampleLink({ - title, - description, - icon, - href, - size = "lg", -}: ExampleLinkProps) { - return ( - - - - ); -} diff --git a/frontend/packages/components/src/actors/getting-started.tsx b/frontend/packages/components/src/actors/getting-started.tsx deleted file mode 100644 index e64f8ac502..0000000000 --- a/frontend/packages/components/src/actors/getting-started.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Icon, faActors } from "@rivet-gg/icons"; -import { useActorsView } from "./actors-view-context-provider"; -import { ActorsResources } from "./get-started"; - -export function GettingStarted() { - const { copy } = useActorsView(); - return ( -
-
- -

- {copy.gettingStarted.title} -

-

- {copy.gettingStarted.description} -

-
- -
- ); -} diff --git a/frontend/public/examples b/frontend/public/examples new file mode 120000 index 0000000000..3d2cdf1426 --- /dev/null +++ b/frontend/public/examples @@ -0,0 +1 @@ +../../website/public/examples \ No newline at end of file diff --git a/frontend/src/app/data-providers/default-data-provider.tsx b/frontend/src/app/data-providers/default-data-provider.tsx index d56dc63240..5b4472ffcb 100644 --- a/frontend/src/app/data-providers/default-data-provider.tsx +++ b/frontend/src/app/data-providers/default-data-provider.tsx @@ -82,6 +82,8 @@ const defaultContext = { refetchInterval: 2000, queryFn: async () => { throw new Error("Not implemented"); + // biome-ignore lint/correctness/noUnreachable: for types + return {} as PaginatedActorResponse; }, getNextPageParam: (lastPage) => { if (lastPage.pagination.cursor) { @@ -109,6 +111,8 @@ const defaultContext = { refetchInterval: 2000, queryFn: async () => { throw new Error("Not implemented"); + // biome-ignore lint/correctness/noUnreachable: for types + return {} as PaginatedBuildsResponse; }, getNextPageParam: () => { return undefined; @@ -328,6 +332,8 @@ const defaultContext = { initialPageParam: null as string | null, queryFn: async () => { throw new Error("Not implemented"); + // biome-ignore lint/correctness/noUnreachable: for types + return {} as PaginatedRegionsResponse; }, getNextPageParam: () => null, select: (data) => data.pages.flatMap((page) => page.regions), @@ -339,6 +345,8 @@ const defaultContext = { enabled: !!regionId, queryFn: async () => { throw new Error("Not implemented"); + // biome-ignore lint/correctness/noUnreachable: for types + return {} as Region; }, }); }, @@ -350,6 +358,8 @@ const defaultContext = { retry: 0, queryFn: async () => { throw new Error("Not implemented"); + // biome-ignore lint/correctness/noUnreachable: for types + return false as boolean; }, }); }, @@ -358,6 +368,8 @@ const defaultContext = { mutationKey: ["createActor"] as QueryKey, mutationFn: async (_: CreateActor) => { throw new Error("Not implemented"); + // biome-ignore lint/correctness/noUnreachable: for types + return {} as string; }, onSuccess: () => { const keys = this.actorsQueryOptions({}).queryKey.filter( diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx index 3a651e6fc3..39243d8213 100644 --- a/frontend/src/app/data-providers/engine-data-provider.tsx +++ b/frontend/src/app/data-providers/engine-data-provider.tsx @@ -2,6 +2,7 @@ import { type Rivet, RivetClient } from "@rivetkit/engine-api-full"; import { fetcher } from "@rivetkit/engine-api-full/core"; import { infiniteQueryOptions, + type MutationKey, mutationOptions, type QueryKey, queryOptions, @@ -354,7 +355,7 @@ export const createNamespaceContext = ({ createActorMutationOptions() { return mutationOptions({ ...def.createActorMutationOptions(), - mutationKey: [namespace, "actors"], + mutationKey: [namespace, "actors"] as MutationKey, mutationFn: async (data) => { const response = await client.actorsCreate({ namespace, diff --git a/frontend/src/app/dialogs/connect-aws-frame.tsx b/frontend/src/app/dialogs/connect-aws-frame.tsx index e63650839b..2f6efaaa89 100644 --- a/frontend/src/app/dialogs/connect-aws-frame.tsx +++ b/frontend/src/app/dialogs/connect-aws-frame.tsx @@ -2,24 +2,32 @@ import { faAws, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; -interface ConnectAwsFrameContentProps extends DialogContentProps {} +interface ConnectAwsFrameContentProps extends DialogContentProps { + footer?: React.ReactNode; + title?: React.ReactNode; +} export default function ConnectAwsFrameContent({ onClose, + footer, + title, }: ConnectAwsFrameContentProps) { return ( <> -
- Add AWS ECS -
+ {title ?? ( +
+ Add AWS ECS +
+ )}
diff --git a/frontend/src/app/dialogs/connect-cloudflare-frame.tsx b/frontend/src/app/dialogs/connect-cloudflare-frame.tsx new file mode 100644 index 0000000000..6a58d0bae8 --- /dev/null +++ b/frontend/src/app/dialogs/connect-cloudflare-frame.tsx @@ -0,0 +1,270 @@ +import { faCloudflare, Icon } from "@rivet-gg/icons"; +import { useMutation, usePrefetchInfiniteQuery } 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 { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + CodeFrame, + CodeGroup, + CodePreview, + type DialogContentProps, + Frame, +} 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 { + endpointSchema, + ServerlessConnectionCheck, +} from "../serverless-connection-check"; +import { useSelectedDatacenter } from "./connect-manual-serverfull-frame"; + +const CLOUDFLARE_MAX_REQUEST_DURATION = 30; + +const stepper = defineStepper( + { + id: "configure", + title: "Configure runner", + 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(1, "Must be at least 1") + .max( + CLOUDFLARE_MAX_REQUEST_DURATION, + "Cloudflare Workers requests time out after 30s", + ), + }), + }, + { + id: "deploy", + title: "Deploy to Cloudflare Workers", + assist: false, + next: "Next", + schema: z.object({}), + }, + { + id: "verify", + title: "Connect & verify", + assist: true, + next: "Add", + schema: z.object({ + endpoint: endpointSchema, + success: z.boolean().refine((v) => v === true, { + message: "Runner must be connected to proceed", + }), + }), + }, +); + +interface ConnectCloudflareFrameContentProps extends DialogContentProps { + title?: React.ReactNode; + footer?: React.ReactNode; +} + +export default function ConnectCloudflareFrameContent({ + onClose, + title, + footer, +}: ConnectCloudflareFrameContentProps) { + usePrefetchInfiniteQuery({ + ...useEngineCompatDataProvider().regionsQueryOptions(), + pages: Infinity, + }); + + return ( + <> + + + {title ?? ( +
+ Add {" "} + Cloudflare Workers +
+ )} +
+
+ + + + + ); +} + +function FormStepper({ + onClose, + footer, +}: { + onClose?: () => void; + footer?: React.ReactNode; +}) { + const provider = useEngineCompatDataProvider(); + + const { mutateAsync } = useMutation({ + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { + confetti({ angle: 60, spread: 55, origin: { x: 0 } }); + confetti({ angle: 120, spread: 55, origin: { x: 1 } }); + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); + onClose?.(); + }, + }); + + return ( + , + deploy: () => , + verify: () => , + }} + onSubmit={async ({ values }) => { + const selectedDatacenters = Object.entries(values.datacenters) + .filter(([, selected]) => selected) + .map(([id]) => id); + + const config = { + serverless: { + url: values.endpoint, + maxRunners: values.maxRunners, + minRunners: values.minRunners, + slotsPerRunner: values.slotsPerRunner, + runnersMargin: values.runnerMargin, + requestLifespan: values.requestLifespan, + headers: Object.fromEntries( + values.headers.map(([key, value]) => [key, value]), + ), + }, + metadata: { provider: "cloudflare-workers" }, + }; + + const payload = Object.fromEntries( + selectedDatacenters.map((dc) => [dc, config]), + ); + + await mutateAsync({ name: values.runnerName, config: payload }); + }} + defaultValues={{ + runnerName: "default", + slotsPerRunner: 1, + minRunners: 1, + maxRunners: 10_000, + runnerMargin: 0, + requestLifespan: 25, + headers: [], + success: false, + endpoint: "", + datacenters: {}, + }} + footer={footer} + /> + ); +} + +function StepConfigure() { + return ( +
+ + + + + + Advanced + + + + + + + + + + + +
+ ); +} + +function StepDeploy() { + const runnerName = useWatch({ name: "runnerName" }); + return ( +
+

+ Make sure your Worker is integrated with RivetKit. See the{" "} + + Cloudflare Workers guide + {" "} + for wiring up Durable Objects. +

+

Set these environment variables in your Wrangler config:

+ +
+

Deploy to Cloudflare's edge:

+ + {[ + "wrangler deploy"} + > + + , + ]} + +
+

+ Use your deployed Worker URL with{" "} + /rivet appended for the + endpoint. +

+
+ ); +} + +function StepVerify() { + return ( + <> +

+ Paste the deployed Worker endpoint (including /rivet) and wait + for the health check to pass. +

+ + + + ); +} diff --git a/frontend/src/app/dialogs/connect-gcp-frame.tsx b/frontend/src/app/dialogs/connect-gcp-frame.tsx index 7ff9e811ac..b1d7844bfb 100644 --- a/frontend/src/app/dialogs/connect-gcp-frame.tsx +++ b/frontend/src/app/dialogs/connect-gcp-frame.tsx @@ -2,24 +2,32 @@ import { faGoogleCloud, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; -interface ConnectAwsFrameContentProps extends DialogContentProps {} +interface ConnectGcpFrameContentProps extends DialogContentProps { + footer?: React.ReactNode; + title?: React.ReactNode; +} -export default function ConnectAwsFrameContent({ +export default function ConnectGcpFrameContent({ onClose, -}: ConnectAwsFrameContentProps) { + footer, + title, +}: ConnectGcpFrameContentProps) { return ( <> -
- Add {" "} - Google Cloud Run -
+ {title ?? ( +
+ Add {" "} + Google Cloud Run +
+ )}
diff --git a/frontend/src/app/dialogs/connect-hetzner-frame.tsx b/frontend/src/app/dialogs/connect-hetzner-frame.tsx index dd85f5292b..aef31e3e86 100644 --- a/frontend/src/app/dialogs/connect-hetzner-frame.tsx +++ b/frontend/src/app/dialogs/connect-hetzner-frame.tsx @@ -2,24 +2,32 @@ import { faHetznerH, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; -interface ConnectHetznerFrameContentProps extends DialogContentProps {} +interface ConnectHetznerFrameContentProps extends DialogContentProps { + footer?: React.ReactNode; + title?: React.ReactNode; +} export default function ConnectHetznerFrameContent({ onClose, + footer, + title, }: ConnectHetznerFrameContentProps) { return ( <> -
- Add {" "} - Hetzner -
+ {title ?? ( +
+ Add {" "} + Hetzner +
+ )}
diff --git a/frontend/src/app/dialogs/connect-k8s-frame.tsx b/frontend/src/app/dialogs/connect-k8s-frame.tsx new file mode 100644 index 0000000000..a9eba9e969 --- /dev/null +++ b/frontend/src/app/dialogs/connect-k8s-frame.tsx @@ -0,0 +1,36 @@ +import { faKubernetes, Icon } from "@rivet-gg/icons"; +import { type DialogContentProps, Frame } from "@/components"; +import ConnectManualServerlfullFrameContent from "./connect-manual-serverfull-frame"; + +interface ConnectK8sFrameContentProps extends DialogContentProps { + footer?: React.ReactNode; + title?: React.ReactNode; +} + +export default function ConnectK8sFrameContent({ + onClose, + footer, + title, +}: ConnectK8sFrameContentProps) { + return ( + <> + + + {title ?? ( +
+ Add {" "} + Kubernetes +
+ )} +
+
+ + + + + ); +} diff --git a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx index e788b4ba5f..ea27ae2ce7 100644 --- a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx @@ -51,11 +51,13 @@ const stepper = defineStepper( interface ConnectManualServerlfullFrameContentProps extends DialogContentProps { provider: string; + footer?: React.ReactNode; } export default function ConnectManualServerlfullFrameContent({ onClose, provider, + footer, }: ConnectManualServerlfullFrameContentProps) { usePrefetchInfiniteQuery({ ...useEngineCompatDataProvider().regionsQueryOptions(), @@ -75,6 +77,7 @@ export default function ConnectManualServerlfullFrameContent({ return ( void; provider: string; defaultDatacenter: string; + footer?: React.ReactNode; }) { const dataProvider = useEngineCompatDataProvider(); @@ -120,6 +125,7 @@ function FormStepper({ return ( { let existing: Record = {}; try { diff --git a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx index c94e1cfe63..e3381e71d4 100644 --- a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx @@ -62,10 +62,15 @@ const stepper = defineStepper( }, ); -interface ConnectManualServerlessFrameContentProps extends DialogContentProps {} +interface ConnectManualServerlessFrameContentProps extends DialogContentProps { + provider: string; + footer?: React.ReactNode; +} export default function ConnectManualServerlessFrameContent({ onClose, + provider, + footer, }: ConnectManualServerlessFrameContentProps) { usePrefetchInfiniteQuery({ ...useEngineCompatDataProvider().regionsQueryOptions(), @@ -76,24 +81,35 @@ export default function ConnectManualServerlessFrameContent({ useEngineCompatDataProvider().regionsQueryOptions(), ); - return ; + return ( + + ); } function FormStepper({ onClose, datacenters, + provider, + footer, }: { onClose?: () => void; datacenters: Region[]; + provider: string; + footer?: React.ReactNode; }) { - const provider = useEngineCompatDataProvider(); + const dataProvider = useEngineCompatDataProvider(); const { data } = useSuspenseInfiniteQuery({ - ...provider.runnerConfigsQueryOptions(), + ...dataProvider.runnerConfigsQueryOptions(), }); const { mutateAsync } = useMutation({ - ...provider.upsertRunnerConfigMutationOptions(), + ...dataProvider.upsertRunnerConfigMutationOptions(), onSuccess: async () => { confetti({ angle: 60, @@ -107,19 +123,20 @@ function FormStepper({ }); await queryClient.invalidateQueries( - provider.runnerConfigsQueryOptions(), + dataProvider.runnerConfigsQueryOptions(), ); onClose?.(); }, }); return ( { let existing: Record = {}; try { const runnerConfig = await queryClient.fetchQuery( - provider.runnerConfigQueryOptions({ + dataProvider.runnerConfigQueryOptions({ name: values.runnerName, }), ); @@ -144,7 +161,7 @@ function FormStepper({ ), }, metadata: { - provider: "custom", + provider, }, }; @@ -176,7 +193,7 @@ function FormStepper({ content={{ "step-1": () => , "step-2": () => , - "step-3": () => , + "step-3": () => , }} /> ); @@ -209,11 +226,11 @@ function Step2() { ); } -function Step3() { +function Step3({ provider }: { provider: string }) { return ( <> - + ); } diff --git a/frontend/src/app/dialogs/connect-quick-railway-frame.tsx b/frontend/src/app/dialogs/connect-quick-railway-frame.tsx index 5a3bf6631c..8910160862 100644 --- a/frontend/src/app/dialogs/connect-quick-railway-frame.tsx +++ b/frontend/src/app/dialogs/connect-quick-railway-frame.tsx @@ -46,10 +46,15 @@ const stepper = defineStepper( }, ); -interface ConnectQuickRailwayFrameContentProps extends DialogContentProps {} +interface ConnectQuickRailwayFrameContentProps extends DialogContentProps { + title?: React.ReactNode; + footer?: React.ReactNode; +} export default function ConnectQuickRailwayFrameContent({ onClose, + title, + footer, }: ConnectQuickRailwayFrameContentProps) { usePrefetchInfiniteQuery({ ...useEngineCompatDataProvider().regionsQueryOptions(), @@ -71,13 +76,17 @@ export default function ConnectQuickRailwayFrameContent({ <> -
- Add Railway -
+ {title ?? ( +
+ Add {" "} + Railway +
+ )}
@@ -89,9 +98,11 @@ export default function ConnectQuickRailwayFrameContent({ function FormStepper({ onClose, defaultDatacenter, + footer, }: { onClose?: () => void; defaultDatacenter: string; + footer?: React.ReactNode; }) { const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ @@ -116,6 +127,7 @@ function FormStepper({ }); return ( { await mutateAsync({ diff --git a/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx b/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx index 7d27a9b877..4d5765d23d 100644 --- a/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx +++ b/frontend/src/app/dialogs/connect-quick-vercel-frame.tsx @@ -25,10 +25,15 @@ import { VERCEL_SERVERLESS_MAX_DURATION } from "./connect-vercel-frame"; const { stepper } = ConnectVercelForm; -interface ConnectQuickVercelFrameContentProps extends DialogContentProps {} +interface ConnectQuickVercelFrameContentProps extends DialogContentProps { + title?: React.ReactNode; + footer?: React.ReactNode; +} export default function ConnectQuickVercelFrameContent({ onClose, + title, + footer, }: ConnectQuickVercelFrameContentProps) { usePrefetchInfiniteQuery({ ...useEngineCompatDataProvider().regionsQueryOptions(), @@ -43,14 +48,20 @@ export default function ConnectQuickVercelFrameContent({ <> -
- Add - Vercel -
+ {title ?? ( +
+ Add + Vercel +
+ )}
- + ); @@ -59,9 +70,11 @@ export default function ConnectQuickVercelFrameContent({ function FormStepper({ datacenters, onClose, + footer, }: { onClose?: () => void; datacenters: Region[]; + footer?: React.ReactNode; }) { const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ @@ -86,6 +99,7 @@ function FormStepper({ return ( t.name === name); + + const navigate = useNavigate(); + + if (!example) { + return ( + +
Example not found.
+
+ ); + } + + if (provider) { + const footer = ( + + ); + + return match(provider) + .with("vercel", () => ( + + Deploy "{example.displayName}" to{" "} + + Vercel + + } + onClose={onClose} + footer={footer} + /> + )) + .with("cloudflare", () => ( + + Deploy "{example.displayName}" to + + Cloudflare + + } + onClose={onClose} + footer={footer} + /> + )) + .with("railway", () => ( + + Deploy "{example.displayName}" to + + Railway + + } + onClose={onClose} + footer={footer} + /> + )) + .with("kubernetes", () => ( + + Deploy "{example.displayName}" to{" "} + Kubernetes + + } + onClose={onClose} + footer={footer} + /> + )) + .with("aws-ecs", () => ( + + Deploy "{example.displayName}" to{" "} + AWS ECS + + } + onClose={onClose} + footer={footer} + /> + )) + .with("gcp-cloud-run", () => ( + + Deploy "{example.displayName}" to{" "} + GCP Cloud Run + + } + onClose={onClose} + footer={footer} + /> + )) + .with("hetzner", () => ( + + Deploy "{example.displayName}" to{" "} + Hetzner + + } + onClose={onClose} + footer={footer} + /> + )) + .with("vm-bare-metal", () => ( + <> + + +
+ Deploy "{example.displayName}" to{" "} + Bare Metal / VM +
+
+
+ + + )) + .otherwise(() => ( + +
Provider {provider} not supported.
+
+ )); + } + + return ( + + ); +} + +function ChooseProvider({ + example, + createProjectOnProviderSelect, +}: { + example: (typeof templates)[number]; + createProjectOnProviderSelect?: boolean; +}) { + const navigate = useNavigate(); + + const { mutateAsync, isPending } = useMutation( + useCloudDataProvider().currentOrgCreateProjectMutationOptions(), + ); + + const [showProviderList, setShowProviderList] = useState(false); + return ( + <> +
+ + +
+

+ Deploy "{example.displayName}" +

+

+ Choose your deployment provider +

+
+
+ + +
+ + + {!showProviderList ? ( + + ) : null} + + {showProviderList + ? deployOptions.slice(1).map((option) => ( + + )) + : null} +
+
+ + ); +} diff --git a/frontend/src/app/forms/connect-manual-serverless-form.tsx b/frontend/src/app/forms/connect-manual-serverless-form.tsx index 13ff1f6897..0312fe19c8 100644 --- a/frontend/src/app/forms/connect-manual-serverless-form.tsx +++ b/frontend/src/app/forms/connect-manual-serverless-form.tsx @@ -1,27 +1,14 @@ -import { - faCheck, - faSpinnerThird, - faTrash, - faTriangleExclamation, - Icon, -} from "@rivet-gg/icons"; -import type { Rivet } from "@rivetkit/engine-api-full"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { AnimatePresence, motion } from "framer-motion"; -import { useEffect } from "react"; -import { - useController, - useFieldArray, - useFormContext, - useWatch, -} from "react-hook-form"; -import { match, P } from "ts-pattern"; -import { useDebounceValue } from "usehooks-ts"; +import { faTrash, Icon } from "@rivet-gg/icons"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useFieldArray, useFormContext } from "react-hook-form"; import z from "zod"; +import { + endpointSchema, + ServerlessConnectionCheck, +} from "@/app/serverless-connection-check"; import { Button, Checkbox, - cn, FormControl, FormDescription, FormField, @@ -36,12 +23,6 @@ import { ActorRegion, useEngineCompatDataProvider } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; import { VisibilitySensor } from "@/components/visibility-sensor"; -const endpointSchema = z - .string() - .nonempty("Endpoint is required") - .url("Please enter a valid URL") - .endsWith("/api/rivet", "Endpoint must end with /api/rivet"); - export const stepper = defineStepper( { id: "step-1", @@ -433,155 +414,10 @@ export const Endpoint = ({ }; export function ConnectionCheck({ provider }: { provider: string }) { - const dataProvider = useEngineCompatDataProvider(); - - const endpoint: string = useWatch({ name: "endpoint" }); - const headers: [string, string][] = useWatch({ name: "headers" }); - - const enabled = !!endpoint && endpointSchema.safeParse(endpoint).success; - - const [debouncedEndpoint] = useDebounceValue(endpoint, 300); - const [debouncedHeaders] = useDebounceValue(headers, 300); - - const { isSuccess, data, isError, isRefetchError, isLoadingError, error } = - useQuery({ - ...dataProvider.runnerHealthCheckQueryOptions({ - runnerUrl: debouncedEndpoint, - headers: Object.fromEntries( - debouncedHeaders - .filter(([k, v]) => k && v) - .map(([k, v]) => [k, v]), - ), - }), - enabled, - retry: 0, - refetchInterval: 3_000, - }); - - const { - field: { onChange }, - } = useController({ name: "success" }); - - useEffect(() => { - onChange(isSuccess); - }, [isSuccess, onChange]); - return ( - - {enabled ? ( - - {isSuccess ? ( - <> - {" "} - {provider} is running with RivetKit{" "} - {(data as any)?.version} - - ) : isError || isRefetchError || isLoadingError ? ( -
-

- {" "} - Health check failed, verify the endpoint is - correct. -

- {isRivetHealthCheckFailureResponse(error) ? ( - - ) : null} -

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

-
- ) : ( -
-
- {" "} - Waiting for Runner to connect... -
-
- )} -
- ) : null} -
+ ); } - -function isRivetHealthCheckFailureResponse( - error: any, -): error is Rivet.RunnerConfigsServerlessHealthCheckResponseFailure["failure"] { - return error && "error" in error; -} - -function HealthCheckFailure({ - error, -}: { - error: Rivet.RunnerConfigsServerlessHealthCheckResponseFailure["failure"]; -}) { - if (!("error" in error)) { - return null; - } - if (!error.error) { - return null; - } - - return match(error.error) - .with({ nonSuccessStatus: P.any }, (e) => { - return ( -

- Health check failed with status{" "} - {e.nonSuccessStatus.statusCode} -

- ); - }) - .with({ invalidRequest: P.any }, (e) => { - return

Health check failed because the request was invalid.

; - }) - .with({ invalidResponseJson: P.any }, (e) => { - return ( -

- Health check failed because the response was not valid JSON. -

- ); - }) - .with({ requestFailed: P.any }, (e) => { - return ( -

- Health check failed because the request could not be - completed. -

- ); - }) - .with({ requestTimedOut: P.any }, (e) => { - return

Health check failed because the request timed out.

; - }) - .with({ invalidResponseSchema: P.any }, (e) => { - return ( -

Health check failed because the response was not valid.

- ); - }) - .exhaustive(); -} diff --git a/frontend/src/app/forms/stepper-form.tsx b/frontend/src/app/forms/stepper-form.tsx index aa43ee159f..1809c78169 100644 --- a/frontend/src/app/forms/stepper-form.tsx +++ b/frontend/src/app/forms/stepper-form.tsx @@ -51,6 +51,7 @@ type StepperFormProps = StepperProps & content: Record ReactNode>; showAllSteps?: boolean; initialStep?: Steps[number]["id"]; + footer?: ReactNode; }; export type StepperFormValues = z.TypeOf< @@ -80,6 +81,7 @@ function Content({ showAllSteps, onSubmit, initialStep, + footer, ...formProps }: StepperFormProps) { const stepper = useStepper({ initialStep }); @@ -125,6 +127,7 @@ function Content({ content={content} showPrevious={false} showControls={steps.length - 1 === index} + footer={footer} /> ) : ( stepper.when(step.id, (step) => { @@ -134,6 +137,7 @@ function Content({ stepper={stepper} step={step} content={content} + footer={footer} /> ); }) @@ -151,13 +155,15 @@ function StepPanel({ stepper, step, content, - showPrevious, + showPrevious = true, showControls = true, + footer, }: Pick, "Stepper" | "content"> & { stepper: Stepperize.Stepper; step: Steps[number]; showControls?: boolean; showPrevious?: boolean; + footer?: ReactNode; }) { const form = useFormContext(); return ( @@ -165,6 +171,7 @@ function StepPanel({ {stepper.match(step.id, content)} {showControls ? ( + {footer} {step.assist ? : null} {showPrevious ? ( + + + + ) : null} + + + ); +} + +function Templates({ + getTemplateLink, +}: { + getTemplateLink?: (template: string) => LinkOptions; +}) { + const [showAll, setShowAll] = useState(false); + return ( + <> + + {templates + .toSorted( + (a, b) => + (a.priority || Number.MAX_SAFE_INTEGER) - + (b.priority || Number.MAX_SAFE_INTEGER), + ) + .slice(0, showAll ? templates.length : 4) + .map((template) => ( + + ))} + +
+ {!showAll && templates.length > 4 && ( + + )} +
+ + ); +} + +function TemplateCard({ + title, + slug, + className, + description, + getLink = (slug) => ({ + to: ".", + search: { modal: "start-with-template", name: slug }, + }), +}: { + title: string; + slug: string; + description?: string; + className?: string; + getLink?: (template: string) => LinkOptions; +}) { + return ( + + ); +} + +const MotionLinkComponent = React.forwardRef< + HTMLAnchorElement, + ComponentProps +>((props, ref) => { + return ; +}); + +const MotionLink = createLink(MotionLinkComponent); + +export function ExamplePreview({ + slug, + title, + className, +}: { + slug: string; + title: string; + className?: string; +}) { + return ( +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+ +
+
+ ); +} diff --git a/frontend/src/app/login.tsx b/frontend/src/app/login.tsx index 6038df9f38..5632c46561 100644 --- a/frontend/src/app/login.tsx +++ b/frontend/src/app/login.tsx @@ -27,7 +27,7 @@ export function Login() { // HACK: redirect if user is already logged in, race condition with clerk useEffect(() => { if (user) { - navigate({ to: "/" }); + navigate({ to: "/", search: true }); } }, [user, navigate]); diff --git a/frontend/src/app/one-click-deploy-railway-button.tsx b/frontend/src/app/one-click-deploy-railway-button.tsx new file mode 100644 index 0000000000..1c4aa4dc0b --- /dev/null +++ b/frontend/src/app/one-click-deploy-railway-button.tsx @@ -0,0 +1,19 @@ +import { faRailway, Icon } from "@rivet-gg/icons"; +import { Link } from "@tanstack/react-router"; +import { Button } from "../components/ui/button"; + +export function OneClickDeployRailwayButton() { + return ( + + ); +} diff --git a/frontend/src/app/runner-config-table.tsx b/frontend/src/app/runner-config-table.tsx index 03063f5b6f..5098148a00 100644 --- a/frontend/src/app/runner-config-table.tsx +++ b/frontend/src/app/runner-config-table.tsx @@ -1,4 +1,5 @@ import { + faCloudflare, faCog, faCogs, faNextjs, @@ -208,7 +209,7 @@ function Row({ ); } -function getModal(provider: string | undefined) { +function getModal(_provider: string | undefined) { return "edit-provider-config"; } @@ -217,6 +218,14 @@ function Provider({ metadata }: { metadata: unknown }) { return Unknown; } if ("provider" in metadata && typeof metadata.provider === "string") { + if (metadata.provider === "cloudflare-workers") { + return ( +
+ Cloudflare + Workers +
+ ); + } if (metadata.provider === "vercel") { return (
diff --git a/frontend/src/app/serverless-connection-check.tsx b/frontend/src/app/serverless-connection-check.tsx new file mode 100644 index 0000000000..f3b3a8f55f --- /dev/null +++ b/frontend/src/app/serverless-connection-check.tsx @@ -0,0 +1,186 @@ +import { + faCheck, + faSpinnerThird, + faTriangleExclamation, + Icon, +} from "@rivet-gg/icons"; +import type { Rivet } from "@rivetkit/engine-api-full"; +import { useQuery } from "@tanstack/react-query"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEffect } from "react"; +import { useController, useWatch } from "react-hook-form"; +import { match, P } from "ts-pattern"; +import { useDebounceValue } from "usehooks-ts"; +import z from "zod"; +import { cn } from "@/components"; +import { useEngineCompatDataProvider } from "@/components/actors"; + +export const endpointSchema = z + .string() + .nonempty("Endpoint is required") + .url("Please enter a valid URL") + .endsWith("/api/rivet", "Endpoint must end with /api/rivet"); + +interface ServerlessConnectionCheckProps { + providerLabel: string; + /** How often to poll the runner health endpoint. */ + pollIntervalMs?: number; +} + +export function ServerlessConnectionCheck({ + providerLabel, + pollIntervalMs = 3_000, +}: ServerlessConnectionCheckProps) { + const dataProvider = useEngineCompatDataProvider(); + + const endpoint: string = useWatch({ name: "endpoint" }); + const headers: [string, string][] = useWatch({ name: "headers" }); + + const enabled = + Boolean(endpoint) && endpointSchema.safeParse(endpoint).success; + + const [debouncedEndpoint] = useDebounceValue(endpoint, 300); + const [debouncedHeaders] = useDebounceValue(headers, 300); + + const { isSuccess, data, isError, isRefetchError, isLoadingError, error } = + useQuery({ + ...dataProvider.runnerHealthCheckQueryOptions({ + runnerUrl: debouncedEndpoint, + headers: Object.fromEntries( + (debouncedHeaders || []) + .filter(([k, v]) => k && v) + .map(([k, v]) => [k, v]), + ), + }), + enabled, + retry: 0, + refetchInterval: pollIntervalMs, + }); + + const { + field: { onChange }, + } = useController({ name: "success" }); + + useEffect(() => { + onChange(isSuccess); + }, [isSuccess, onChange]); + + return ( + + {enabled ? ( + + {isSuccess ? ( + <> + + {providerLabel} is running with RivetKit{" "} + {(data as any)?.version} + + ) : isError || isRefetchError || isLoadingError ? ( +
+

+ + Health check failed, verify the endpoint is + correct. +

+ {isRivetHealthCheckFailureResponse(error) ? ( + + ) : null} +

+ Endpoint{" "} + + {endpoint} + +

+
+ ) : ( +
+
+ + Waiting for Runner to connect... +
+
+ )} +
+ ) : null} +
+ ); +} + +function isRivetHealthCheckFailureResponse( + error: any, +): error is Rivet.RunnerConfigsServerlessHealthCheckResponseFailure["failure"] { + return error && "error" in error; +} + +function HealthCheckFailure({ + error, +}: { + error: Rivet.RunnerConfigsServerlessHealthCheckResponseFailure["failure"]; +}) { + if (!("error" in error)) { + return null; + } + if (!error.error) { + return null; + } + + return match(error.error) + .with({ nonSuccessStatus: P.any }, (e) => { + return ( +

+ Health check failed with status{" "} + {e.nonSuccessStatus.statusCode} +

+ ); + }) + .with({ invalidRequest: P.any }, () => { + return

Health check failed because the request was invalid.

; + }) + .with({ invalidResponseJson: P.any }, () => { + return ( +

+ Health check failed because the response was not valid JSON. +

+ ); + }) + .with({ requestFailed: P.any }, () => { + return ( +

+ Health check failed because the request could not be + completed. +

+ ); + }) + .with({ requestTimedOut: P.any }, () => { + return

Health check failed because the request timed out.

; + }) + .with({ invalidResponseSchema: P.any }, () => { + return ( +

Health check failed because the response was not valid.

+ ); + }) + .exhaustive(); +} diff --git a/frontend/src/app/use-dialog.tsx b/frontend/src/app/use-dialog.tsx index 14f84484b7..9ecc317bce 100644 --- a/frontend/src/app/use-dialog.tsx +++ b/frontend/src/app/use-dialog.tsx @@ -23,6 +23,9 @@ export const useDialog = { ConnectManual: createDialogHook( () => import("@/app/dialogs/connect-manual-frame"), ), + ConnectCloudflare: createDialogHook( + () => import("@/app/dialogs/connect-cloudflare-frame"), + ), ConnectAws: createDialogHook( () => import("@/app/dialogs/connect-aws-frame"), ), @@ -47,4 +50,7 @@ export const useDialog = { CreateApiToken: createDialogHook( () => import("@/app/dialogs/create-api-token-frame"), ), + StartWithTemplate: createDialogHook( + () => import("@/app/dialogs/start-with-template-frame"), + ), }; diff --git a/frontend/src/components/cloud-organization-select.tsx b/frontend/src/components/cloud-organization-select.tsx new file mode 100644 index 0000000000..24c7161f81 --- /dev/null +++ b/frontend/src/components/cloud-organization-select.tsx @@ -0,0 +1,100 @@ +import { useOrganizationList } from "@clerk/clerk-react"; +import { faPlus, Icon } from "@rivet-gg/icons"; +import { type ComponentProps, useCallback } from "react"; +import { + Avatar, + AvatarFallback, + AvatarImage, + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, + Skeleton, +} from "@/components"; +import { VisibilitySensor } from "@/components/visibility-sensor"; + +interface CloudOrganizationSelectProps extends ComponentProps { + showCreateOrganization?: boolean; + onCreateClick?: () => void; +} + +export function CloudOrganizationSelect({ + showCreateOrganization, + onCreateClick, + onValueChange, + ...props +}: CloudOrganizationSelectProps) { + const { + userMemberships: { data = [], isLoading, hasNextPage, fetchNext }, + } = useOrganizationList({ + userMemberships: { + infinite: true, + }, + }); + + const handleValueChange = useCallback( + (value: string) => { + if (value === "create") { + onCreateClick?.(); + return; + } + onValueChange?.(value); + }, + [onCreateClick, onValueChange], + ); + + return ( + + ); +} diff --git a/frontend/src/components/cloud-project-select.tsx b/frontend/src/components/cloud-project-select.tsx new file mode 100644 index 0000000000..16d8cfa8aa --- /dev/null +++ b/frontend/src/components/cloud-project-select.tsx @@ -0,0 +1,92 @@ +import { faPlus, Icon } from "@rivet-gg/icons"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { type ComponentProps, useCallback } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, + Skeleton, +} from "@/components"; +import { useCloudDataProvider } from "@/components/actors"; +import { VisibilitySensor } from "@/components/visibility-sensor"; + +interface CloudProjectSelectProps extends ComponentProps { + organization: string; + showCreateProject?: boolean; + onCreateClick?: () => void; +} + +export function CloudProjectSelect({ + showCreateProject, + onCreateClick, + onValueChange, + organization, + ...props +}: CloudProjectSelectProps) { + const dataProvider = useCloudDataProvider(); + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery( + dataProvider.projectsQueryOptions({ + organization, + }), + ); + + const handleValueChange = useCallback( + (value: string) => { + if (value === "create") { + onCreateClick?.(); + return; + } + onValueChange?.(value); + }, + [onCreateClick, onValueChange], + ); + + return ( + + ); +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index af2fd6dd30..7ade501343 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -4,6 +4,8 @@ export { toast } from "sonner"; export * from "./action-card"; export * from "./animated-currency"; export * from "./asset-image"; +export * from "./cloud-organization-select"; +export * from "./cloud-project-select"; export * from "./code"; export * from "./code-preview/code-preview"; export * from "./copy-area"; diff --git a/frontend/src/components/tailwind-base.ts b/frontend/src/components/tailwind-base.ts index a11d21957c..ee158c781a 100644 --- a/frontend/src/components/tailwind-base.ts +++ b/frontend/src/components/tailwind-base.ts @@ -52,6 +52,16 @@ const config = { "monospace", ], }, + backgroundImage: { + "card-fade": + "linear-gradient(to bottom, transparent 0%, transparent 30%, hsl(var(--card) / 30%) 60%, hsl(var(--card) / 100%) 100%)", + }, + dropShadow: { + "dialog-close": [ + "0 0 8px rgb(0 0 0 / 1)", + "0 4px 3px rgb(0 0 0 / 1)", + ], + }, data: { active: 'status~="active"', open: 'state*="open"', diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index 476e4323c1..57d6da82e0 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -48,7 +48,10 @@ const DialogContent = React.forwardRef< {children} {hideClose ? null : ( - + Close )} diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 8da8f0304e..60f649baa2 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -7,11 +7,15 @@ export const clerk = ? new Clerk(cloudEnv().VITE_APP_CLERK_PUBLISHABLE_KEY) : (null as unknown as Clerk); -export const redirectToOrganization = async (clerk: Clerk) => { +export const redirectToOrganization = async ( + clerk: Clerk, + search: Record, +) => { if (clerk.user) { if (clerk.organization) { throw redirect({ - to: "/orgs/$organization", + to: search.from ?? "/orgs/$organization", + search: true, params: { organization: clerk.organization.id, }, @@ -22,12 +26,14 @@ export const redirectToOrganization = async (clerk: Clerk) => { if (orgs.length > 0) { await clerk.setActive({ organization: orgs[0].organization.id }); throw redirect({ - to: "/orgs/$organization", + to: search.from ?? "/orgs/$organization", + search: true, params: { organization: orgs[0].organization.id }, }); } throw redirect({ to: "/onboarding/choose-organization", + search: true, }); } diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 619fe8d6fb..ab97c0a52b 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -19,12 +19,15 @@ import { Route as OnboardingChooseOrganizationRouteImport } from './routes/onboa import { Route as OnboardingAcceptInvitationRouteImport } from './routes/onboarding/accept-invitation' import { Route as ContextEngineRouteImport } from './routes/_context/_engine' import { Route as ContextCloudRouteImport } from './routes/_context/_cloud' +import { Route as ContextCloudNewIndexRouteImport } from './routes/_context/_cloud/new/index' import { Route as ContextEngineNsNamespaceRouteImport } from './routes/_context/_engine/ns.$namespace' import { Route as ContextCloudOrgsOrganizationRouteImport } from './routes/_context/_cloud/orgs.$organization' +import { Route as ContextCloudNewTemplateRouteImport } from './routes/_context/_cloud/new/$template' import { Route as ContextEngineNsNamespaceIndexRouteImport } from './routes/_context/_engine/ns.$namespace/index' import { Route as ContextCloudOrgsOrganizationIndexRouteImport } from './routes/_context/_cloud/orgs.$organization/index' import { Route as ContextEngineNsNamespaceConnectRouteImport } from './routes/_context/_engine/ns.$namespace/connect' import { Route as ContextCloudOrgsOrganizationProjectsIndexRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.index' +import { Route as ContextCloudOrgsOrganizationNewIndexRouteImport } from './routes/_context/_cloud/orgs.$organization/new/index' import { Route as ContextCloudOrgsOrganizationProjectsProjectRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project' import { Route as ContextCloudOrgsOrganizationProjectsProjectIndexRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/index' import { Route as ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteImport } from './routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace' @@ -81,6 +84,11 @@ const ContextCloudRoute = ContextCloudRouteImport.update({ id: '/_cloud', getParentRoute: () => ContextRoute, } as any) +const ContextCloudNewIndexRoute = ContextCloudNewIndexRouteImport.update({ + id: '/new/', + path: '/new/', + getParentRoute: () => ContextCloudRoute, +} as any) const ContextEngineNsNamespaceRoute = ContextEngineNsNamespaceRouteImport.update({ id: '/ns/$namespace', @@ -93,6 +101,11 @@ const ContextCloudOrgsOrganizationRoute = path: '/orgs/$organization', getParentRoute: () => ContextCloudRoute, } as any) +const ContextCloudNewTemplateRoute = ContextCloudNewTemplateRouteImport.update({ + id: '/new/$template', + path: '/new/$template', + getParentRoute: () => ContextCloudRoute, +} as any) const ContextEngineNsNamespaceIndexRoute = ContextEngineNsNamespaceIndexRouteImport.update({ id: '/', @@ -117,6 +130,12 @@ const ContextCloudOrgsOrganizationProjectsIndexRoute = path: '/projects/', getParentRoute: () => ContextCloudOrgsOrganizationRoute, } as any) +const ContextCloudOrgsOrganizationNewIndexRoute = + ContextCloudOrgsOrganizationNewIndexRouteImport.update({ + id: '/new/', + path: '/new/', + getParentRoute: () => ContextCloudOrgsOrganizationRoute, + } as any) const ContextCloudOrgsOrganizationProjectsProjectRoute = ContextCloudOrgsOrganizationProjectsProjectRouteImport.update({ id: '/projects/$project', @@ -171,12 +190,15 @@ export interface FileRoutesByFullPath { '/onboarding/accept-invitation': typeof OnboardingAcceptInvitationRoute '/onboarding/choose-organization': typeof OnboardingChooseOrganizationRoute '/': typeof ContextIndexRoute + '/new/$template': typeof ContextCloudNewTemplateRoute '/orgs/$organization': typeof ContextCloudOrgsOrganizationRouteWithChildren '/ns/$namespace': typeof ContextEngineNsNamespaceRouteWithChildren + '/new': typeof ContextCloudNewIndexRoute '/ns/$namespace/connect': typeof ContextEngineNsNamespaceConnectRoute '/orgs/$organization/': typeof ContextCloudOrgsOrganizationIndexRoute '/ns/$namespace/': typeof ContextEngineNsNamespaceIndexRoute '/orgs/$organization/projects/$project': typeof ContextCloudOrgsOrganizationProjectsProjectRouteWithChildren + '/orgs/$organization/new': typeof ContextCloudOrgsOrganizationNewIndexRoute '/orgs/$organization/projects': typeof ContextCloudOrgsOrganizationProjectsIndexRoute '/orgs/$organization/projects/$project/': typeof ContextCloudOrgsOrganizationProjectsProjectIndexRoute '/orgs/$organization/projects/$project/ns/$namespace': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteWithChildren @@ -192,9 +214,12 @@ export interface FileRoutesByTo { '/onboarding/accept-invitation': typeof OnboardingAcceptInvitationRoute '/onboarding/choose-organization': typeof OnboardingChooseOrganizationRoute '/': typeof ContextIndexRoute + '/new/$template': typeof ContextCloudNewTemplateRoute + '/new': typeof ContextCloudNewIndexRoute '/ns/$namespace/connect': typeof ContextEngineNsNamespaceConnectRoute '/orgs/$organization': typeof ContextCloudOrgsOrganizationIndexRoute '/ns/$namespace': typeof ContextEngineNsNamespaceIndexRoute + '/orgs/$organization/new': typeof ContextCloudOrgsOrganizationNewIndexRoute '/orgs/$organization/projects': typeof ContextCloudOrgsOrganizationProjectsIndexRoute '/orgs/$organization/projects/$project': typeof ContextCloudOrgsOrganizationProjectsProjectIndexRoute '/orgs/$organization/projects/$project/ns/$namespace/connect': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceConnectRoute @@ -213,12 +238,15 @@ export interface FileRoutesById { '/onboarding/accept-invitation': typeof OnboardingAcceptInvitationRoute '/onboarding/choose-organization': typeof OnboardingChooseOrganizationRoute '/_context/': typeof ContextIndexRoute + '/_context/_cloud/new/$template': typeof ContextCloudNewTemplateRoute '/_context/_cloud/orgs/$organization': typeof ContextCloudOrgsOrganizationRouteWithChildren '/_context/_engine/ns/$namespace': typeof ContextEngineNsNamespaceRouteWithChildren + '/_context/_cloud/new/': typeof ContextCloudNewIndexRoute '/_context/_engine/ns/$namespace/connect': typeof ContextEngineNsNamespaceConnectRoute '/_context/_cloud/orgs/$organization/': typeof ContextCloudOrgsOrganizationIndexRoute '/_context/_engine/ns/$namespace/': typeof ContextEngineNsNamespaceIndexRoute '/_context/_cloud/orgs/$organization/projects/$project': typeof ContextCloudOrgsOrganizationProjectsProjectRouteWithChildren + '/_context/_cloud/orgs/$organization/new/': typeof ContextCloudOrgsOrganizationNewIndexRoute '/_context/_cloud/orgs/$organization/projects/': typeof ContextCloudOrgsOrganizationProjectsIndexRoute '/_context/_cloud/orgs/$organization/projects/$project/': typeof ContextCloudOrgsOrganizationProjectsProjectIndexRoute '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace': typeof ContextCloudOrgsOrganizationProjectsProjectNsNamespaceRouteWithChildren @@ -236,12 +264,15 @@ export interface FileRouteTypes { | '/onboarding/accept-invitation' | '/onboarding/choose-organization' | '/' + | '/new/$template' | '/orgs/$organization' | '/ns/$namespace' + | '/new' | '/ns/$namespace/connect' | '/orgs/$organization/' | '/ns/$namespace/' | '/orgs/$organization/projects/$project' + | '/orgs/$organization/new' | '/orgs/$organization/projects' | '/orgs/$organization/projects/$project/' | '/orgs/$organization/projects/$project/ns/$namespace' @@ -257,9 +288,12 @@ export interface FileRouteTypes { | '/onboarding/accept-invitation' | '/onboarding/choose-organization' | '/' + | '/new/$template' + | '/new' | '/ns/$namespace/connect' | '/orgs/$organization' | '/ns/$namespace' + | '/orgs/$organization/new' | '/orgs/$organization/projects' | '/orgs/$organization/projects/$project' | '/orgs/$organization/projects/$project/ns/$namespace/connect' @@ -277,12 +311,15 @@ export interface FileRouteTypes { | '/onboarding/accept-invitation' | '/onboarding/choose-organization' | '/_context/' + | '/_context/_cloud/new/$template' | '/_context/_cloud/orgs/$organization' | '/_context/_engine/ns/$namespace' + | '/_context/_cloud/new/' | '/_context/_engine/ns/$namespace/connect' | '/_context/_cloud/orgs/$organization/' | '/_context/_engine/ns/$namespace/' | '/_context/_cloud/orgs/$organization/projects/$project' + | '/_context/_cloud/orgs/$organization/new/' | '/_context/_cloud/orgs/$organization/projects/' | '/_context/_cloud/orgs/$organization/projects/$project/' | '/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace' @@ -371,6 +408,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ContextCloudRouteImport parentRoute: typeof ContextRoute } + '/_context/_cloud/new/': { + id: '/_context/_cloud/new/' + path: '/new' + fullPath: '/new' + preLoaderRoute: typeof ContextCloudNewIndexRouteImport + parentRoute: typeof ContextCloudRoute + } '/_context/_engine/ns/$namespace': { id: '/_context/_engine/ns/$namespace' path: '/ns/$namespace' @@ -385,6 +429,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ContextCloudOrgsOrganizationRouteImport parentRoute: typeof ContextCloudRoute } + '/_context/_cloud/new/$template': { + id: '/_context/_cloud/new/$template' + path: '/new/$template' + fullPath: '/new/$template' + preLoaderRoute: typeof ContextCloudNewTemplateRouteImport + parentRoute: typeof ContextCloudRoute + } '/_context/_engine/ns/$namespace/': { id: '/_context/_engine/ns/$namespace/' path: '/' @@ -413,6 +464,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ContextCloudOrgsOrganizationProjectsIndexRouteImport parentRoute: typeof ContextCloudOrgsOrganizationRoute } + '/_context/_cloud/orgs/$organization/new/': { + id: '/_context/_cloud/orgs/$organization/new/' + path: '/new' + fullPath: '/orgs/$organization/new' + preLoaderRoute: typeof ContextCloudOrgsOrganizationNewIndexRouteImport + parentRoute: typeof ContextCloudOrgsOrganizationRoute + } '/_context/_cloud/orgs/$organization/projects/$project': { id: '/_context/_cloud/orgs/$organization/projects/$project' path: '/projects/$project' @@ -500,6 +558,7 @@ const ContextCloudOrgsOrganizationProjectsProjectRouteWithChildren = interface ContextCloudOrgsOrganizationRouteChildren { ContextCloudOrgsOrganizationIndexRoute: typeof ContextCloudOrgsOrganizationIndexRoute ContextCloudOrgsOrganizationProjectsProjectRoute: typeof ContextCloudOrgsOrganizationProjectsProjectRouteWithChildren + ContextCloudOrgsOrganizationNewIndexRoute: typeof ContextCloudOrgsOrganizationNewIndexRoute ContextCloudOrgsOrganizationProjectsIndexRoute: typeof ContextCloudOrgsOrganizationProjectsIndexRoute } @@ -509,6 +568,8 @@ const ContextCloudOrgsOrganizationRouteChildren: ContextCloudOrgsOrganizationRou ContextCloudOrgsOrganizationIndexRoute, ContextCloudOrgsOrganizationProjectsProjectRoute: ContextCloudOrgsOrganizationProjectsProjectRouteWithChildren, + ContextCloudOrgsOrganizationNewIndexRoute: + ContextCloudOrgsOrganizationNewIndexRoute, ContextCloudOrgsOrganizationProjectsIndexRoute: ContextCloudOrgsOrganizationProjectsIndexRoute, } @@ -519,12 +580,16 @@ const ContextCloudOrgsOrganizationRouteWithChildren = ) interface ContextCloudRouteChildren { + ContextCloudNewTemplateRoute: typeof ContextCloudNewTemplateRoute ContextCloudOrgsOrganizationRoute: typeof ContextCloudOrgsOrganizationRouteWithChildren + ContextCloudNewIndexRoute: typeof ContextCloudNewIndexRoute } const ContextCloudRouteChildren: ContextCloudRouteChildren = { + ContextCloudNewTemplateRoute: ContextCloudNewTemplateRoute, ContextCloudOrgsOrganizationRoute: ContextCloudOrgsOrganizationRouteWithChildren, + ContextCloudNewIndexRoute: ContextCloudNewIndexRoute, } const ContextCloudRouteWithChildren = ContextCloudRoute._addFileChildren( diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 4cc72fbbc9..b7a9b761c5 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,6 +1,5 @@ import type { Clerk } from "@clerk/clerk-js"; import { ClerkProvider } from "@clerk/clerk-react"; -import * as ClerkComponents from "@clerk/elements/common"; import { dark } from "@clerk/themes"; import type { QueryClient } from "@tanstack/react-query"; import { diff --git a/frontend/src/routes/_context/_cloud.tsx b/frontend/src/routes/_context/_cloud.tsx index d9779d4d27..ba83f8fe41 100644 --- a/frontend/src/routes/_context/_cloud.tsx +++ b/frontend/src/routes/_context/_cloud.tsx @@ -56,7 +56,7 @@ function CloudModals() { // FIXME onOpenChange: (value: any) => { if (!value) { - navigate({ + return navigate({ to: ".", search: (old) => ({ ...old, @@ -73,7 +73,7 @@ function CloudModals() { // FIXME onOpenChange: (value: any) => { if (!value) { - navigate({ + return navigate({ to: ".", search: (old) => ({ ...old, diff --git a/frontend/src/routes/_context/_cloud/new/$template.tsx b/frontend/src/routes/_context/_cloud/new/$template.tsx new file mode 100644 index 0000000000..72ec5c75fd --- /dev/null +++ b/frontend/src/routes/_context/_cloud/new/$template.tsx @@ -0,0 +1,22 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_context/_cloud/new/$template")({ + component: RouteComponent, + beforeLoad: async ({ context, params }) => { + throw redirect({ + to: "/orgs/$organization/new", + params: { + organization: context.clerk.organization?.id ?? "", + ...params, + }, + search: { + template: params.template, + modal: "get-started", + }, + }); + }, +}); + +function RouteComponent() { + return null; +} diff --git a/frontend/src/routes/_context/_cloud/new/index.tsx b/frontend/src/routes/_context/_cloud/new/index.tsx new file mode 100644 index 0000000000..6e8d937615 --- /dev/null +++ b/frontend/src/routes/_context/_cloud/new/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_context/_cloud/new/")({ + component: RouteComponent, + beforeLoad: async ({ context, params }) => { + throw redirect({ + to: "/orgs/$organization/new", + params: { + organization: context.clerk.organization?.id ?? "", + ...params, + }, + }) + }, +}); + +function RouteComponent() { + return null; +} diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/index.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/index.tsx index e53637fd21..0f4b835d25 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/index.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/index.tsx @@ -3,7 +3,7 @@ import { match } from "ts-pattern"; import CreateProjectFrameContent from "@/app/dialogs/create-project-frame"; import { RouteError } from "@/app/route-error"; import { PendingRouteLayout, RouteLayout } from "@/app/route-layout"; -import { Card, H2, Skeleton } from "@/components"; +import { Card } from "@/components"; export const Route = createFileRoute("/_context/_cloud/orgs/$organization/")({ loader: async ({ context, params }) => { @@ -22,7 +22,7 @@ export const Route = createFileRoute("/_context/_cloud/orgs/$organization/")({ throw redirect({ to: "/orgs/$organization/projects/$project", replace: true, - + search: true, params: { organization: params.organization, project: firstProject.name, diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/new/index.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/new/index.tsx new file mode 100644 index 0000000000..ae8b20460e --- /dev/null +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/new/index.tsx @@ -0,0 +1,48 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { GettingStarted } from "@/app/getting-started"; +import { RouteLayout } from "@/app/route-layout"; +import { useDialog } from "@/app/use-dialog"; + +export const Route = createFileRoute( + "/_context/_cloud/orgs/$organization/new/", +)({ + component: RouteComponent, +}); + +function RouteComponent() { + const navigate = Route.useNavigate(); + const search = Route.useSearch(); + const StartWithTemplateDialog = useDialog.StartWithTemplate.Dialog; + return ( + <> + + ({ + to: "/orgs/$organization/new", + search: { template: slug, modal: "get-started" }, + })} + /> + + { + if (!value) { + return navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> + + ); +} diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/index.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/index.tsx index 03cc5c06c4..b39087470f 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/index.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/index.tsx @@ -7,7 +7,7 @@ import { Card } from "@/components"; export const Route = createFileRoute( "/_context/_cloud/orgs/$organization/projects/$project/", )({ - beforeLoad: ({ context, params }) => { + beforeLoad: ({ context, params, search }) => { return match(__APP_TYPE__) .with("cloud", async () => { if (!context.clerk?.organization) { @@ -23,7 +23,7 @@ export const Route = createFileRoute( throw redirect({ to: "/orgs/$organization/projects/$project/ns/$namespace", replace: true, - + search, params: { organization: params.organization, project: params.project, diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx index 524d044634..539b429540 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx @@ -1,7 +1,12 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { + createFileRoute, + useNavigate, + useSearch, +} from "@tanstack/react-router"; import { createNamespaceContext } from "@/app/data-providers/cloud-data-provider"; import { NotFoundCard } from "@/app/not-found-card"; import { PendingRouteLayout, RouteLayout } from "@/app/route-layout"; +import { useDialog } from "@/app/use-dialog"; export const Route = createFileRoute( "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace", @@ -36,5 +41,38 @@ export const Route = createFileRoute( }); function RouteComponent() { - return ; + return ( + <> + + + + ); +} + +function CloudNamespaceModals() { + const navigate = useNavigate(); + const search = useSearch({ from: "/_context" }); + const StartWithTemplateDialog = useDialog.StartWithTemplate.Dialog; + + return ( + { + if (!value) { + return navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + name: undefined, + }), + }); + } + }, + }} + /> + ); } diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx index 1b6b5ca41b..3373d76432 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/connect.tsx @@ -2,12 +2,9 @@ import { faAws, faGoogleCloud, faHetznerH, - faNextjs, - faNodeJs, faPlus, faQuestionCircle, faRailway, - faReact, faServer, faVercel, Icon, @@ -17,21 +14,15 @@ import { useSuspenseInfiniteQuery, useSuspenseQuery, } from "@tanstack/react-query"; -import { - createFileRoute, - Link, - notFound, - Link as RouterLink, - useParams, -} from "@tanstack/react-router"; +import { createFileRoute, notFound, useParams } from "@tanstack/react-router"; import { match } from "ts-pattern"; +import { GettingStarted } from "@/app/getting-started"; import { HelpDropdown } from "@/app/help-dropdown"; import { PublishableTokenCodeGroup } from "@/app/publishable-token-code-group"; import { RunnerConfigsTable } from "@/app/runner-config-table"; import { RunnersTable } from "@/app/runners-table"; import { Button, - DocsSheet, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -109,196 +100,7 @@ export function RouteComponent() { } if (!hasRunnerNames) { - return ( -
-
-
-
-

Create New Project

-
-

- Start a new RivetKit project with Rivet Cloud. Use - one of our templates to get started quickly. -

- -
-
-

1-Click Deploy From Template

-
- - -
-
-
-

Quickstart Guides

-
- - - - - - - - - -
-
-
-
-
-

Connect Existing Project

- - - -
-

- Connect your RivetKit application to Rivet Cloud. - Use your cloud of choice to run Rivet Actors. -

- -
-
-

Add Provider

-
- - - - - - - -
-
-
-
-
- ); + return ; } return ( @@ -586,19 +388,3 @@ function DataLoadingPlaceholder() {
); } - -function OneClickDeployRailwayButton() { - return ( - - ); -} diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/index.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/index.tsx index e9f5e6a0c3..34c6a83603 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/index.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/index.tsx @@ -16,7 +16,7 @@ export const Route = createFileRoute( "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/", )({ component: RouteComponent, - beforeLoad: async ({ context }) => { + beforeLoad: async ({ context, search }) => { if (context.__type !== "cloud") { throw notFound(); } @@ -24,7 +24,12 @@ export const Route = createFileRoute( const shouldDisplay = await shouldDisplayActors(context); if (!shouldDisplay) { - throw redirect({ from: Route.to, replace: true, to: "./connect" }); + throw redirect({ + from: Route.to, + replace: true, + to: "./connect", + search, + }); } }, }); diff --git a/frontend/src/routes/_context/_engine.tsx b/frontend/src/routes/_context/_engine.tsx index 085a73dd1b..5d39a69a06 100644 --- a/frontend/src/routes/_context/_engine.tsx +++ b/frontend/src/routes/_context/_engine.tsx @@ -44,6 +44,7 @@ function EngineModals() { const ConnectHetznerDialog = useDialog.ConnectHetzner.Dialog; const EditProviderConfigDialog = useDialog.EditProviderConfig.Dialog; const DeleteConfigDialog = useDialog.DeleteConfig.Dialog; + const StartWithTemplateDialog = useDialog.StartWithTemplate.Dialog; return ( <> @@ -265,6 +266,23 @@ function EngineModals() { }, }} /> + { + if (!value) { + navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> ); } diff --git a/frontend/src/routes/_context/index.tsx b/frontend/src/routes/_context/index.tsx index cbf583265e..50bc9b4b4b 100644 --- a/frontend/src/routes/_context/index.tsx +++ b/frontend/src/routes/_context/index.tsx @@ -17,8 +17,8 @@ export const Route = createFileRoute("/_context/")({ beforeLoad: async ({ context, search }) => { return await match(context) .with({ __type: "cloud" }, async () => { - if (!(await redirectToOrganization(context.clerk))) { - throw redirect({ to: "/login" }); + if (!(await redirectToOrganization(context.clerk, search))) { + throw redirect({ to: "/login", search: true }); } }) .with({ __type: "engine" }, async (ctx) => { diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 7ba8e2a2d0..bde6cc6f24 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -6,9 +6,9 @@ import { waitForClerk } from "@/lib/waitForClerk"; export const Route = createFileRoute("/login")({ component: RouteComponent, - beforeLoad: async ({ context }) => { + beforeLoad: async ({ context, search }) => { await waitForClerk(context.clerk); - await redirectToOrganization(context.clerk); + await redirectToOrganization(context.clerk, search); }, }); diff --git a/frontend/src/routes/onboarding/choose-organization.tsx b/frontend/src/routes/onboarding/choose-organization.tsx index 2e72c7635a..c354eca3d3 100644 --- a/frontend/src/routes/onboarding/choose-organization.tsx +++ b/frontend/src/routes/onboarding/choose-organization.tsx @@ -1,42 +1,30 @@ -import { CreateOrganization, useOrganizationList } from "@clerk/clerk-react"; -import { createFileRoute, Navigate } from "@tanstack/react-router"; +import { createFileRoute, redirect } from "@tanstack/react-router"; import { RouteLayout } from "@/app/route-layout"; export const Route = createFileRoute("/onboarding/choose-organization")({ component: RouteComponent, + beforeLoad: async ({ context }) => { + const org = await context.clerk.createOrganization({ + name: `${context.clerk.user?.firstName}'s Organization`, + }); + + await context.clerk.setActive({ organization: org.id }); + await context.clerk.session?.reload(); + + throw redirect({ + to: "/orgs/$organization", + params: { organization: org.id }, + }); + }, }); function RouteComponent() { - const { - userMemberships: { data: userMemberships }, - } = useOrganizationList({ userMemberships: true }); - return (
- {userMemberships?.length ? ( - - ) : null} - - `/orgs/${org.id}` - } - appearance={{ - variables: { - colorBackground: "hsl(var(--card))", - }, - }} - /> + Creating your organization...
diff --git a/frontend/vite.engine.config.ts b/frontend/vite.engine.config.ts index 934a7d08c8..18655da977 100644 --- a/frontend/vite.engine.config.ts +++ b/frontend/vite.engine.config.ts @@ -4,6 +4,7 @@ import { sentryVitePlugin } from "@sentry/vite-plugin"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import favigo from "favigo/vite"; +import Macros from "unplugin-macros/vite"; import { defineConfig, loadEnv, type Plugin } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; import { commonEnvSchema } from "./src/lib/env"; @@ -58,6 +59,7 @@ export default defineConfig(({ mode }) => { background: "transparent", }, }), + Macros(), ], server: { port: 43708, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48c6faafea..6211e99141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1584,6 +1584,9 @@ importers: '@rivetkit/engine-api-full': specifier: workspace:* version: link:../engine/sdks/typescript/api-full + '@rivetkit/example-registry': + specifier: workspace:^ + version: link:packages/example-registry '@sentry/react': specifier: ^8.55.0 version: 8.55.0(react@19.1.1) @@ -1791,6 +1794,9 @@ importers: typescript-plugin-css-modules: specifier: ^5.2.0 version: 5.2.0(typescript@5.9.2) + unplugin-macros: + specifier: ^0.18.3 + version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.1) @@ -2311,7 +2317,7 @@ importers: version: 5.9.2 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) @@ -2396,7 +2402,7 @@ importers: version: 15.5.6(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)) '@next/third-parties': specifier: latest - version: 16.0.3(next@15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0) + version: 16.0.8(next@15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0) '@rivet-gg/api': specifier: 25.5.3 version: 25.5.3 @@ -5500,8 +5506,8 @@ packages: cpu: [x64] os: [win32] - '@next/third-parties@16.0.3': - resolution: {integrity: sha512-QjTRQ4ydXguFkpMCUMl5oSslxmh8mAtmnzc9DEtLkZcGmAuQcZg2M3lswMn62sdID+F06crS3IQ58X3sjjBLVA==} + '@next/third-parties@16.0.8': + resolution: {integrity: sha512-F8TNI1GFm7ivZX6HBMDVsDklAXOHlACbtw7w3WFbDk2oFXdVgKZBfdK+ILhjTBK4ibIXzLMWO1Py3bgSETKHYQ==} peerDependencies: next: ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0 react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -8000,6 +8006,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -10964,6 +10974,10 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -11510,6 +11524,7 @@ packages: next@15.4.5: resolution: {integrity: sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -11531,6 +11546,7 @@ packages: next@15.5.6: resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -13773,6 +13789,10 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin-macros@0.18.3: + resolution: {integrity: sha512-wTIAXuT/+yLxVtgIxpeNUtjb4pzebL5W7EkJwXMYlY4OqJIuFOEAsitMyX3exitT/4DtXjycs3sg+N5SIXMECw==} + engines: {node: '>=20.18.0'} + unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} @@ -14642,7 +14662,7 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 @@ -14658,7 +14678,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -14717,7 +14737,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -14755,7 +14775,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} @@ -14789,7 +14809,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -17204,7 +17224,7 @@ snapshots: '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -17552,7 +17572,7 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.6': optional: true - '@next/third-parties@16.0.3(next@15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0)': + '@next/third-parties@16.0.8(next@15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0)': dependencies: next: 15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2) react: 19.2.0 @@ -19466,7 +19486,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@types/babel__traverse@7.28.0': @@ -20635,6 +20655,11 @@ snapshots: assertion-error@2.0.1: {} + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.28.5 + pathe: 2.0.3 + ast-types-flow@0.0.8: {} ast-types@0.13.4: @@ -22078,7 +22103,7 @@ snapshots: eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@1.21.7)) @@ -22111,7 +22136,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -22126,7 +22151,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -24052,6 +24077,10 @@ snapshots: lunr@2.3.9: {} + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.19 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -27990,6 +28019,27 @@ snapshots: unpipe@1.0.0: {} + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + ast-kit: 2.2.0 + magic-string-ast: 1.0.3 + unplugin: 2.3.10 + vite: 7.2.2(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - ms + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + unplugin@1.0.1: dependencies: acorn: 8.15.0 @@ -28308,6 +28358,27 @@ snapshots: - supports-color - terser + vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + es-module-lexer: 1.7.0 + obug: 2.0.0(ms@2.1.3) + pathe: 2.0.3 + vite: 7.2.2(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - ms + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vite-node@5.2.0(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(ms@2.1.3)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -28380,13 +28451,13 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: - vite: 7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - supports-color - typescript @@ -28501,7 +28572,7 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1): + vite@7.2.2(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -28510,7 +28581,7 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 20.19.13 fsevents: 2.3.3 jiti: 1.21.7 less: 4.4.1 @@ -28518,9 +28589,8 @@ snapshots: sass: 1.93.2 stylus: 0.62.0 terser: 5.44.0 - tsx: 4.20.5 + tsx: 4.20.6 yaml: 2.8.1 - optional: true vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx index 16d563624b..ce2fd6e087 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx @@ -54,7 +54,7 @@ export const IntegrationsSection = () => (

Infrastructure

- {deployOptions.map(({ title, shortTitle, href }) => ( + {deployOptions.map(({ displayName: title, shortTitle, href }) => ( option.title !== "Vercel" && option.title !== "Railway" + option => option.displayName !== "Vercel" && option.displayName !== "Railway" ); return ( @@ -45,7 +45,7 @@ export function DeployDropdown() {
{otherPlatforms.map((option) => ( setIsOpen(false)} > {option.icon && } - {option.shortTitle || option.title} + {option.shortTitle || option.displayName} ))}
diff --git a/website/src/components/docs/Hosting.tsx b/website/src/components/docs/Hosting.tsx index ea870d343e..9da470bfbf 100644 --- a/website/src/components/docs/Hosting.tsx +++ b/website/src/components/docs/Hosting.tsx @@ -21,7 +21,7 @@ export function Hosting() { {hostingProviders .filter((x) => !x.specializedPlatform) - .map(({ title, href, icon }) => ( + .map(({ displayName: title, href, icon }) => ( ({ title: groupTitle, - pages: items.map(({ title, href, icon, badge }) => ({ + pages: items.map(({ displayName: title, href, icon, badge }) => ({ title, href, icon,