diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx index 7a2aa97d00..02567053c0 100644 --- a/frontend/src/app/data-providers/engine-data-provider.tsx +++ b/frontend/src/app/data-providers/engine-data-provider.tsx @@ -527,7 +527,7 @@ export const createNamespaceContext = ({ }, }); }, - createRunnerConfigMutationOptions( + upsertRunnerConfigMutationOptions( opts: { onSuccess?: (data: Rivet.RunnerConfigsUpsertResponse) => void; } = {}, @@ -554,6 +554,21 @@ export const createNamespaceContext = ({ }, }; }, + deleteRunnerConfigMutationOptions( + opts: { onSuccess?: (data: void) => void } = {}, + ) { + return { + ...opts, + mutationKey: ["runner-config", "delete"], + mutationFn: async (name: string) => { + await client.runnerConfigs.delete(name, { namespace }); + }, + retry: shouldRetryAllExpect403, + meta: { + mightRequireAuth, + }, + }; + }, runnerConfigsQueryOptions() { return infiniteQueryOptions({ queryKey: [{ namespace }, "runners", "configs"], @@ -592,6 +607,7 @@ export const createNamespaceContext = ({ }, }); }, + runnerConfigQueryOptions(runnerName: string) { return queryOptions({ queryKey: [{ namespace }, "runners", "config", runnerName], diff --git a/frontend/src/app/dialogs/confirm-delete-config-frame.tsx b/frontend/src/app/dialogs/confirm-delete-config-frame.tsx new file mode 100644 index 0000000000..004b647da3 --- /dev/null +++ b/frontend/src/app/dialogs/confirm-delete-config-frame.tsx @@ -0,0 +1,53 @@ +import { useMutation } from "@tanstack/react-query"; +import { Button, type DialogContentProps, Frame } from "@/components"; +import { useEngineCompatDataProvider } from "@/components/actors"; +import { queryClient } from "@/queries/global"; + +interface ConfirmDeleteConfigContentProps extends DialogContentProps { + name: string; +} + +export default function ConfirmDeleteConfigContent({ + onClose, + name, +}: ConfirmDeleteConfigContentProps) { + const provider = useEngineCompatDataProvider(); + const { mutate, isPending } = useMutation( + provider.deleteRunnerConfigMutationOptions({ + onSuccess: async () => { + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); + onClose?.(); + }, + }), + ); + + return ( + <> + + +
Confirm deletion of {name}
+
+ + This action cannot be undone. Are you sure you want to + delete this configuration? + +
+ + + + + + ); +} diff --git a/frontend/src/app/dialogs/connect-aws-frame.tsx b/frontend/src/app/dialogs/connect-aws-frame.tsx index 6493ee18a7..54f581ea96 100644 --- a/frontend/src/app/dialogs/connect-aws-frame.tsx +++ b/frontend/src/app/dialogs/connect-aws-frame.tsx @@ -39,6 +39,7 @@ const stepper = defineStepper( 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"), }), }, @@ -99,9 +100,10 @@ function FormStepper({ onClose?: () => void; datacenters: Region[]; }) { + const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...useEngineCompatDataProvider().createRunnerConfigMutationOptions(), - onSuccess: () => { + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { confetti({ angle: 60, spread: 55, @@ -112,6 +114,10 @@ function FormStepper({ spread: 55, origin: { x: 1 }, }); + + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); onClose?.(); }, }); @@ -166,6 +172,7 @@ function Step1() { + ); diff --git a/frontend/src/app/dialogs/connect-gcp-frame.tsx b/frontend/src/app/dialogs/connect-gcp-frame.tsx index 97956c2186..c5b46a2395 100644 --- a/frontend/src/app/dialogs/connect-gcp-frame.tsx +++ b/frontend/src/app/dialogs/connect-gcp-frame.tsx @@ -13,6 +13,7 @@ import * as ConnectVercelForm from "@/app/forms/connect-vercel-form"; import { cn, type DialogContentProps, Frame } from "@/components"; import { type Region, useEngineCompatDataProvider } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; +import { queryClient } from "@/queries/global"; import { StepperForm } from "../forms/stepper-form"; import { EnvVariablesStep } from "./connect-railway-frame"; @@ -33,6 +34,7 @@ const stepper = defineStepper( 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"), }), }, @@ -94,9 +96,10 @@ function FormStepper({ onClose?: () => void; datacenters: Region[]; }) { + const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...useEngineCompatDataProvider().createRunnerConfigMutationOptions(), - onSuccess: () => { + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { confetti({ angle: 60, spread: 55, @@ -107,6 +110,10 @@ function FormStepper({ spread: 55, origin: { x: 1 }, }); + + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); onClose?.(); }, }); @@ -161,6 +168,7 @@ function Step1() { + ); diff --git a/frontend/src/app/dialogs/connect-hetzner-frame.tsx b/frontend/src/app/dialogs/connect-hetzner-frame.tsx index 5c0e5f9854..b28b18ecb1 100644 --- a/frontend/src/app/dialogs/connect-hetzner-frame.tsx +++ b/frontend/src/app/dialogs/connect-hetzner-frame.tsx @@ -19,6 +19,7 @@ import * as ConnectVercelForm from "@/app/forms/connect-vercel-form"; import { cn, type DialogContentProps, Frame } from "@/components"; import { type Region, useEngineCompatDataProvider } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; +import { queryClient } from "@/queries/global"; import { StepperForm } from "../forms/stepper-form"; import { EnvVariablesStep } from "./connect-railway-frame"; @@ -39,6 +40,7 @@ const stepper = defineStepper( 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"), }), }, @@ -99,9 +101,10 @@ function FormStepper({ onClose?: () => void; datacenters: Region[]; }) { + const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...useEngineCompatDataProvider().createRunnerConfigMutationOptions(), - onSuccess: () => { + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { confetti({ angle: 60, spread: 55, @@ -112,6 +115,10 @@ function FormStepper({ spread: 55, origin: { x: 1 }, }); + + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); onClose?.(); }, }); @@ -143,6 +150,7 @@ function FormStepper({ slotsPerRunner: 25, maxRunners: 1000, runnerMargin: 0, + minRunners: 1, headers: [], success: false, datacenters: Object.fromEntries( @@ -166,6 +174,7 @@ function Step1() { + ); diff --git a/frontend/src/app/dialogs/connect-manual-frame.tsx b/frontend/src/app/dialogs/connect-manual-frame.tsx index f7deb813be..3ef8ca34fe 100644 --- a/frontend/src/app/dialogs/connect-manual-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-frame.tsx @@ -13,6 +13,7 @@ import * as ConnectVercelForm from "@/app/forms/connect-vercel-form"; import { cn, type DialogContentProps, Frame } from "@/components"; import { type Region, useEngineCompatDataProvider } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; +import { queryClient } from "@/queries/global"; import { StepperForm } from "../forms/stepper-form"; import { EnvVariablesStep } from "./connect-railway-frame"; @@ -33,6 +34,7 @@ const stepper = defineStepper( 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"), }), }, @@ -93,9 +95,10 @@ function FormStepper({ onClose?: () => void; datacenters: Region[]; }) { + const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...useEngineCompatDataProvider().createRunnerConfigMutationOptions(), - onSuccess: () => { + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { confetti({ angle: 60, spread: 55, @@ -106,6 +109,10 @@ function FormStepper({ spread: 55, origin: { x: 1 }, }); + + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); onClose?.(); }, }); @@ -136,6 +143,7 @@ function FormStepper({ runnerName: "default", slotsPerRunner: 25, maxRunners: 1000, + minRunners: 1, runnerMargin: 0, headers: [], success: false, @@ -160,6 +168,7 @@ function Step1() { + ); diff --git a/frontend/src/app/dialogs/connect-railway-frame.tsx b/frontend/src/app/dialogs/connect-railway-frame.tsx index 3139a0bee1..ffda9f0548 100644 --- a/frontend/src/app/dialogs/connect-railway-frame.tsx +++ b/frontend/src/app/dialogs/connect-railway-frame.tsx @@ -48,7 +48,7 @@ const stepper = defineStepper( { id: "step-3", title: "Wait for the Runner to connect", - assist: false, + assist: true, schema: z.object({ success: z.boolean().refine((v) => v === true, { message: "Runner must be connected to proceed", @@ -74,6 +74,8 @@ export default function ConnectRailwayFrameContent({ const prefferedRegionForRailway = data.find((region) => region.name.toLowerCase().includes("us-west")) ?.id || + data.find((region) => region.name.toLowerCase().includes("us-east")) + ?.id || data.find((region) => region.name.toLowerCase().includes("ore"))?.id || "auto"; @@ -103,9 +105,10 @@ function FormStepper({ onClose?: () => void; defaultDatacenter: string; }) { + const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...useEngineCompatDataProvider().createRunnerConfigMutationOptions(), - onSuccess: () => { + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { confetti({ angle: 60, spread: 55, @@ -116,6 +119,8 @@ function FormStepper({ spread: 55, origin: { x: 1 }, }); + + await queryClient.invalidateQueries(provider.runnerConfigsQueryOptions()); onClose?.(); }, }); diff --git a/frontend/src/app/dialogs/connect-vercel-frame.tsx b/frontend/src/app/dialogs/connect-vercel-frame.tsx index 439db56138..b6a63a7471 100644 --- a/frontend/src/app/dialogs/connect-vercel-frame.tsx +++ b/frontend/src/app/dialogs/connect-vercel-frame.tsx @@ -20,6 +20,7 @@ import { } from "@/components"; import { type Region, useEngineCompatDataProvider } from "@/components/actors"; import { type JoinStepSchemas, StepperForm } from "../forms/stepper-form"; +import { queryClient } from "@/queries/global"; const { stepper } = ConnectVercelForm; @@ -47,11 +48,6 @@ export default function CreateProjectFrameContent({ Add Vercel - - - @@ -68,9 +64,10 @@ function FormStepper({ onClose?: () => void; datacenters: Region[]; }) { + const provider = useEngineCompatDataProvider(); const { mutateAsync } = useMutation({ - ...useEngineCompatDataProvider().createRunnerConfigMutationOptions(), - onSuccess: () => { + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: async () => { confetti({ angle: 60, spread: 55, @@ -81,6 +78,7 @@ function FormStepper({ spread: 55, origin: { x: 1 }, }); + await queryClient.invalidateQueries(provider.runnerConfigsQueryOptions()); onClose?.(); }, }); @@ -127,6 +125,7 @@ function FormStepper({ plan: "hobby", runnerName: "default", slotsPerRunner: 25, + minRunners: 1, maxRunners: 1000, runnerMargin: 0, headers: [], @@ -153,6 +152,7 @@ function Step1() { + diff --git a/frontend/src/app/dialogs/edit-runner-config.tsx b/frontend/src/app/dialogs/edit-runner-config.tsx index cad6867363..67d49ec8fc 100644 --- a/frontend/src/app/dialogs/edit-runner-config.tsx +++ b/frontend/src/app/dialogs/edit-runner-config.tsx @@ -1,61 +1,100 @@ -import { faQuestionCircle, faRailway, Icon } from "@rivet-gg/icons"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { + useMutation, + usePrefetchInfiniteQuery, + useSuspenseInfiniteQuery, +} from "@tanstack/react-query"; import * as EditRunnerConfigForm from "@/app/forms/edit-runner-config-form"; -import { HelpDropdown } from "@/app/help-dropdown"; -import { Button, type DialogContentProps, Frame } from "@/components"; +import { type DialogContentProps, Frame } from "@/components"; import { useEngineCompatDataProvider } from "@/components/actors"; +import { queryClient } from "@/queries/global"; interface EditRunnerConfigFrameContentProps extends DialogContentProps { name: string; + dc: string; } export default function EditRunnerConfigFrameContent({ name, + dc, onClose, }: EditRunnerConfigFrameContentProps) { - const { data } = useSuspenseQuery({ - ...useEngineCompatDataProvider().runnerConfigQueryOptions(name), - refetchInterval: 5000, + const provider = useEngineCompatDataProvider(); + usePrefetchInfiniteQuery({ + ...provider.runnerConfigsQueryOptions(), + pages: Infinity, }); + const { data } = useSuspenseInfiniteQuery({ + ...provider.runnerConfigsQueryOptions(), + }); + + const { mutateAsync } = useMutation({ + ...provider.upsertRunnerConfigMutationOptions(), + onSuccess: () => { + onClose?.(); + }, + }); + + const config = data.find(([id]) => id === name)?.[1].datacenters?.[dc] + .serverless; + + if (!config) { + return ( + + Selected provider config is not available in this datacenter. + + ); + } + return ( { + onSubmit={async (values) => { + await mutateAsync({ + name, + config: { + [dc]: { + serverless: { + ...values, + headers: Object.fromEntries( + values.headers || [], + ), + }, + }, + }, + }); + + await queryClient.invalidateQueries( + provider.runnerConfigsQueryOptions(), + ); onClose?.(); }} defaultValues={{ - url: data.serverless.url, - maxRunners: data.serverless.maxRunners, - minRunners: data.serverless.minRunners, - requestLifespan: data.serverless.requestLifespan, - runnersMargin: data.serverless.runnersMargin, - slotsPerRunner: data.serverless.slotsPerRunner, + url: config.url, + maxRunners: config.maxRunners, + minRunners: config.minRunners, + requestLifespan: config.requestLifespan, + runnersMargin: config.runnersMargin, + slotsPerRunner: config.slotsPerRunner, }} > -
- Add Railway -
- - - + Edit {name} Provider
- + -
+
-
-
+
- +
- + + +
Save diff --git a/frontend/src/app/forms/connect-railway-form.tsx b/frontend/src/app/forms/connect-railway-form.tsx index 0ba054504c..e75811d019 100644 --- a/frontend/src/app/forms/connect-railway-form.tsx +++ b/frontend/src/app/forms/connect-railway-form.tsx @@ -10,6 +10,7 @@ import * as ConnectVercelForm from "@/app/forms/connect-vercel-form"; import { cn, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -30,10 +31,15 @@ export const Datacenter = function Datacenter() { Datacenter + + You can find the region your Railway runners are running + in under Settings > Deploy + )} diff --git a/frontend/src/app/forms/connect-vercel-form.tsx b/frontend/src/app/forms/connect-vercel-form.tsx index 7a10f93fda..34e0b4a0df 100644 --- a/frontend/src/app/forms/connect-vercel-form.tsx +++ b/frontend/src/app/forms/connect-vercel-form.tsx @@ -68,6 +68,7 @@ export const stepper = defineStepper( 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"), }), }, @@ -81,7 +82,7 @@ export const stepper = defineStepper( { id: "step-3", title: "Deploy to Vercel", - assist: false, + assist: true, next: "Done", schema: z.object({ success: z.boolean().refine((val) => val, "Connection failed"), @@ -190,6 +191,33 @@ export const Datacenters = function Datacenter() { ); }; +export const MinRunners = ({ className }: { className?: string }) => { + const { control } = useFormContext(); + return ( + ( + + Min Runners + + + + + The minimum number of runners to keep running. + + + + )} + /> + ); +}; + export const MaxRunners = ({ className }: { className?: string }) => { const { control } = useFormContext(); return ( diff --git a/frontend/src/app/forms/edit-runner-config-form.tsx b/frontend/src/app/forms/edit-runner-config-form.tsx index 6213e1f1df..a6cdf61671 100644 --- a/frontend/src/app/forms/edit-runner-config-form.tsx +++ b/frontend/src/app/forms/edit-runner-config-form.tsx @@ -1,22 +1,32 @@ -import { type UseFormReturn, useFormContext } from "react-hook-form"; +import { faTrash, Icon } from "@rivet-gg/icons"; +import { + type UseFormReturn, + useFieldArray, + useFormContext, +} from "react-hook-form"; import z from "zod"; import { + Button, createSchemaForm, FormControl, + FormDescription, FormField, + FormFieldContext, FormItem, FormLabel, FormMessage, Input, + Label, } from "@/components"; export const formSchema = z.object({ - url: z.string().url(), - maxRunners: z.number().positive(), - minRunners: z.number().positive(), - requestLifespan: z.number().positive(), - runnersMargin: z.number().positive(), - slotsPerRunner: z.number().positive(), + url: z.string().url().endsWith("/api/rivet"), + maxRunners: z.coerce.number().positive(), + minRunners: z.coerce.number().min(0), + requestLifespan: z.coerce.number().positive(), + runnersMargin: z.coerce.number().min(0), + slotsPerRunner: z.coerce.number().positive(), + headers: z.array(z.string()).default([]), }); export type FormValues = z.infer; @@ -36,7 +46,7 @@ export const Url = ({ className }: { className?: string }) => { name="url" render={({ field }) => ( - Url + Endpoint { return ( ( Min Runners + + The minimum number of runners to keep running. + )} @@ -82,6 +95,11 @@ export const MaxRunners = ({ className }: { className?: string }) => { + + + The maximum number of runners that can be created to + handle load. + )} @@ -103,6 +121,10 @@ export const RequestLifespan = ({ className }: { className?: string }) => { + + The maximum duration (in seconds) a request can take + before being terminated. + )} @@ -122,6 +144,10 @@ export const RunnersMargin = ({ className }: { className?: string }) => { + + The number of extra runners to keep running to handle + sudden spikes in load. + )} @@ -143,9 +169,134 @@ export const SlotsPerRunner = ({ className }: { className?: string }) => { + + The number of concurrent slots each runner can handle. + )} /> ); }; + +export const Headers = function Headers() { + const { control, setValue } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + name: "headers", + control, + }); + + return ( +
+ +

Custom Headers

+
+ + Custom headers to add to each request to the runner. Useful for + providing authentication or other information. + +
+ {fields.length > 0 ? ( + <> + + +

+ + ) : null} + {fields.map((field, index) => ( +
+ + + + + { + setValue( + `headers.${index}.0`, + e.target.value, + { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }, + ); + }} + /> + + + + + + + + + + { + setValue( + `headers.${index}.1`, + e.target.value, + { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }, + ); + }} + /> + + + + + +
+ ))} +
+ +
+ ); +}; diff --git a/frontend/src/app/runner-config-table.tsx b/frontend/src/app/runner-config-table.tsx index 33c5b2bac2..0c20cc762b 100644 --- a/frontend/src/app/runner-config-table.tsx +++ b/frontend/src/app/runner-config-table.tsx @@ -1,5 +1,13 @@ +import { + faCog, + faCogs, + faRailway, + faTrash, + faVercel, + Icon, +} from "@rivet-gg/icons"; import type { Rivet } from "@rivetkit/engine-api-full"; -import { formatRelative } from "date-fns"; +import { Link } from "@tanstack/react-router"; import { Button, DiscreteCopyButton, @@ -13,13 +21,15 @@ import { Text, WithTooltip, } from "@/components"; +import { ActorRegion } from "@/components/actors"; +import { REGION_LABEL } from "@/components/matchmaker/lobby-region"; interface RunnerConfigsTableProps { isLoading?: boolean; isError?: boolean; hasNextPage?: boolean; fetchNextPage?: () => void; - configs: Rivet.RunnerConfig[]; + configs: [string, Rivet.RunnerConfigsListResponseRunnerConfigsValue][]; } export function RunnerConfigsTable({ @@ -35,7 +45,8 @@ export function RunnerConfigsTable({ Name Provider - Endpoint + Endpoint + Datacenter @@ -70,8 +81,8 @@ export function RunnerConfigsTable({ ) : null} - {configs?.map((config) => ( - + {configs?.map(([id, config]) => ( + ))} {!isLoading && hasNextPage ? ( @@ -112,34 +123,135 @@ function RowSkeleton() { ); } -function Row(config: Rivet.RunnerConfig) { +function Row({ + name, + ...value +}: { name: string } & Rivet.RunnerConfigsListResponseRunnerConfigsValue) { + const config = Object.values(value.datacenters)[0]; + return ( - + + + {name} + + + + - {runner.runnerId.slice(0, 8)} + + + {config.serverless?.url && + config.serverless.url.length > 32 + ? `${config.serverless.url.slice(0, 16)}...${config.serverless.url.slice(-16)}` + : config.serverless?.url} + } /> - {config.metadata && - typeof config.metadata === "object" && - "provider" in config.metadata - ? (config.metadata.provider as string) - : "unknown"} + - - - {runner.datacenter} - {runner.remainingSlots}/{runner.totalSlots} +
+ {config.serverless ? ( + + + + + + } + /> + ) : null} + + + + + + } + /> +
- - {formatRelative(runner.createTs, new Date())}
); } + +function getModal(provider: string | undefined) { + return "edit-provider-config"; +} + +function Provider({ metadata }: { metadata: unknown }) { + if (!metadata || typeof metadata !== "object") { + return Unknown; + } + if ("provider" in metadata && typeof metadata.provider === "string") { + if (metadata.provider === "vercel") { + return ( +
+ Vercel +
+ ); + } + if (metadata.provider === "railway") { + return ( +
+ Railway +
+ ); + } + return {metadata.provider || "-"}; + } + return Unknown; +} + +function Regions({ regions }: { regions: string[] }) { + if (regions.length === 1) { + return ( + + ); + } + + return ( + REGION_LABEL[region] ?? REGION_LABEL.unknown) + .join(", ")} + trigger={ + + {regions.length} regions + + } + /> + ); +} diff --git a/frontend/src/app/use-dialog.tsx b/frontend/src/app/use-dialog.tsx index 176ab6c0d6..6248c85a67 100644 --- a/frontend/src/app/use-dialog.tsx +++ b/frontend/src/app/use-dialog.tsx @@ -26,6 +26,12 @@ export const useDialog = { ConnectHetzner: createDialogHook( () => import("@/app/dialogs/connect-hetzner-frame"), ), + EditProviderConfig: createDialogHook( + () => import("@/app/dialogs/edit-runner-config"), + ), + DeleteConfig: createDialogHook( + () => import("@/app/dialogs/confirm-delete-config-frame"), + ), Billing: createDialogHook(() => import("@/app/dialogs/billing-frame")), ProvideEngineCredentials: createDialogHook( () => import("@/app/dialogs/provide-engine-credentials-frame"), diff --git a/frontend/src/components/actors/region-select.tsx b/frontend/src/components/actors/region-select.tsx index 02d8fe6e53..34cdde1dc9 100644 --- a/frontend/src/components/actors/region-select.tsx +++ b/frontend/src/components/actors/region-select.tsx @@ -6,9 +6,14 @@ import { useDataProvider } from "./data-provider"; interface RegionSelectProps { onValueChange: (value: string) => void; value: string; + showAuto?: boolean; } -export function RegionSelect({ onValueChange, value }: RegionSelectProps) { +export function RegionSelect({ + onValueChange, + value, + showAuto = true, +}: RegionSelectProps) { const { data = [], fetchNextPage, @@ -17,11 +22,15 @@ export function RegionSelect({ onValueChange, value }: RegionSelectProps) { } = useInfiniteQuery(useDataProvider().regionsQueryOptions()); const regions = [ - { - label: Automatic (Recommended), - value: "auto", - region: { id: "auto", name: "Automatic" }, - }, + ...(showAuto + ? [ + { + label: Automatic (Recommended), + value: "auto", + region: { id: "auto", name: "Automatic" }, + }, + ] + : []), ...data.map((region) => { return { label: diff --git a/frontend/src/components/matchmaker/lobby-region.tsx b/frontend/src/components/matchmaker/lobby-region.tsx index eb67988f4d..c49672db91 100644 --- a/frontend/src/components/matchmaker/lobby-region.tsx +++ b/frontend/src/components/matchmaker/lobby-region.tsx @@ -48,6 +48,10 @@ export const REGION_ICON: Record = { gru: "🇧🇷", // Sao Paulo bom: "🇮🇳", // Mumbai sin: "🇸🇬", // Singapore + "eu-central-1": "🇩🇪", // Frankfurt + "us-east-1": "🇺🇸", // Northern Virginia + "us-west-1": "🇺🇸", // Oregon + "ap-southeast-1": "🇸🇬", // Singapore }; export const REGION_LABEL: Record = { @@ -96,12 +100,14 @@ export const REGION_LABEL: Record = { gru: "Sao Paulo", bom: "Mumbai, India", sin: "Singapore", + "eu-central-1": "Frankfurt, Germany", + "us-east-1": "Northern Virginia, USA", + "us-west-1": "Oregon, USA", + "ap-southeast-1": "Singapore", }; export function getRegionKey(regionNameId: string | undefined) { - // HACK: Remove prefix for old regions with format `lnd-atl` - const regionIdSplit = (regionNameId || "").split("-"); - return regionIdSplit[regionIdSplit.length - 1]; + return regionNameId; } export function RegionIcon({ diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 13a8d5c97c..8da8f0304e 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -20,6 +20,7 @@ export const redirectToOrganization = async (clerk: Clerk) => { const { data: orgs } = await clerk.user.getOrganizationMemberships(); if (orgs.length > 0) { + await clerk.setActive({ organization: orgs[0].organization.id }); throw redirect({ to: "/orgs/$organization", params: { organization: orgs[0].organization.id }, diff --git a/frontend/src/routes/_context/_cloud.tsx b/frontend/src/routes/_context/_cloud.tsx index 06627800d0..040fe0c24e 100644 --- a/frontend/src/routes/_context/_cloud.tsx +++ b/frontend/src/routes/_context/_cloud.tsx @@ -43,6 +43,8 @@ function CloudModals() { const ConnectAwsDialog = useDialog.ConnectAws.Dialog; const ConnectGcpDialog = useDialog.ConnectGcp.Dialog; const ConnectHetznerDialog = useDialog.ConnectHetzner.Dialog; + const EditProviderConfigDialog = useDialog.EditProviderConfig.Dialog; + const DeleteConfigDialog = useDialog.DeleteConfig.Dialog; const TokensDialog = useDialog.Tokens.Dialog; return ( @@ -218,6 +220,46 @@ function CloudModals() { }, }} /> + { + if (!value) { + navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> + { + if (!value) { + navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> ); } diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization.tsx index f490701682..c741ad5e7a 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization.tsx @@ -4,21 +4,28 @@ import { createOrganizationContext } from "@/app/data-providers/cloud-data-provi export const Route = createFileRoute("/_context/_cloud/orgs/$organization")({ component: RouteComponent, - beforeLoad: ({ context, params }) => { - return match(context) - .with({ __type: "cloud" }, (context) => ({ - dataProvider: { - ...context.dataProvider, - ...createOrganizationContext({ + beforeLoad: async ({ context, params }) => { + return await match(context) + .with({ __type: "cloud" }, async (context) => { + context.clerk.setActive({ + organization: params.organization, + }); + return { + dataProvider: { ...context.dataProvider, - organization: params.organization, - }), - }, - })) + ...createOrganizationContext({ + ...context.dataProvider, + organization: params.organization, + }), + }, + }; + }) .otherwise(() => { throw new Error("Invalid context type for this route"); }); }, + pendingMinMs: 0, + pendingMs: 0, }); function RouteComponent() { 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 bac00f7aa2..56d20e242d 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 @@ -42,34 +42,37 @@ export const Route = createFileRoute( }); export function RouteComponent() { - const { data: runnerNamesCount, isLoading } = useInfiniteQuery({ - ...useEngineCompatDataProvider().runnerNamesQueryOptions(), - select: (data) => data.pages[0].names?.length, + const { data: runnerConfigsCount, isLoading } = useInfiniteQuery({ + ...useEngineCompatDataProvider().runnerConfigsQueryOptions(), + select: (data) => Object.values(data.pages[0].runnerConfigs).length, refetchInterval: 5000, }); - return ( -
-
-

Connect

-
- - - + const hasConfigs = + runnerConfigsCount !== undefined && runnerConfigsCount > 0; + + if (isLoading) { + return ( +
+
+

Connect

+
+ + + +
-
-

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

+

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

-
- {isLoading ? ( +
@@ -81,103 +84,152 @@ export function RouteComponent() {
- ) : runnerNamesCount === 0 ? ( -
-

Add Provider

-
- + +
+
+

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

+ +
+
+

Add Provider

+
+ - + - + + + AWS ECS + + - - + + + Hetzner + + + +
+
+
+
+ ); + } + + return ( +
+
+

Connect

+
+ -
+
- ) : ( - <> - - - - )} +
+

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

+ +
+ + +
); } @@ -196,7 +248,19 @@ function Providers() { return (
-

Providers

+
+

Providers

+ + + + +
@@ -228,17 +292,7 @@ function Runners() { return (
-

Runners

- - - - +

Runners

diff --git a/frontend/src/routes/_context/_engine.tsx b/frontend/src/routes/_context/_engine.tsx index 3125de09be..2ccffa1bf5 100644 --- a/frontend/src/routes/_context/_engine.tsx +++ b/frontend/src/routes/_context/_engine.tsx @@ -40,6 +40,8 @@ function EngineModals() { const ConnectAwsDialog = useDialog.ConnectAws.Dialog; const ConnectGcpDialog = useDialog.ConnectGcp.Dialog; const ConnectHetznerDialog = useDialog.ConnectHetzner.Dialog; + const EditProviderConfigDialog = useDialog.EditProviderConfig.Dialog; + const DeleteConfigDialog = useDialog.DeleteConfig.Dialog; return ( <> @@ -180,6 +182,47 @@ function EngineModals() { }, }} /> + + { + if (!value) { + navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> + { + if (!value) { + navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }, + }} + /> ); } diff --git a/frontend/src/routes/onboarding/choose-organization.tsx b/frontend/src/routes/onboarding/choose-organization.tsx index 8b56a13f97..2e72c7635a 100644 --- a/frontend/src/routes/onboarding/choose-organization.tsx +++ b/frontend/src/routes/onboarding/choose-organization.tsx @@ -1,5 +1,5 @@ -import { CreateOrganization } from "@clerk/clerk-react"; -import { createFileRoute } from "@tanstack/react-router"; +import { CreateOrganization, useOrganizationList } from "@clerk/clerk-react"; +import { createFileRoute, Navigate } from "@tanstack/react-router"; import { RouteLayout } from "@/app/route-layout"; export const Route = createFileRoute("/onboarding/choose-organization")({ @@ -7,11 +7,25 @@ export const Route = createFileRoute("/onboarding/choose-organization")({ }); function RouteComponent() { + const { + userMemberships: { data: userMemberships }, + } = useOrganizationList({ userMemberships: true }); + return (
+ {userMemberships?.length ? ( + + ) : null}