diff --git a/frontend/src/app/dialogs/connect-vercel-frame.tsx b/frontend/src/app/dialogs/connect-vercel-frame.tsx index 394201adbb..1e512e53d4 100644 --- a/frontend/src/app/dialogs/connect-vercel-frame.tsx +++ b/frontend/src/app/dialogs/connect-vercel-frame.tsx @@ -46,20 +46,6 @@ const useEndpoint = () => { }); }; -const useNamespace = () => { - const routeContext = useRouteContext({ - from: "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace/connect", - select: (ctx) => ctx.dataProvider.engineNamespace, - }); - - return match(__APP_TYPE__) - .with("cloud", () => routeContext) - .with("engine", () => "default") - .otherwise(() => { - throw new Error("Not in a valid context"); - }); -}; - export default function CreateProjectFrameContent({ onClose, }: CreateProjectFrameContentProps) { @@ -99,7 +85,7 @@ function FormStepper({ const provider = useEngineCompatDataProvider(); const token = usePublishableToken(); const endpoint = useEndpoint(); - const namespace = useNamespace(); + const namespace = provider.engineNamespace; const { mutateAsync } = useMutation({ ...provider.upsertRunnerConfigMutationOptions(), diff --git a/frontend/src/app/dialogs/edit-runner-config.tsx b/frontend/src/app/dialogs/edit-runner-config.tsx index e3bf87ea98..9063a89d4e 100644 --- a/frontend/src/app/dialogs/edit-runner-config.tsx +++ b/frontend/src/app/dialogs/edit-runner-config.tsx @@ -54,8 +54,6 @@ export default function EditRunnerConfigFrameContent({ .filter(([k, v]) => v.serverless) .map(([k, v]) => [k, config]); - console.log(otherDcs, [dc, config]); - await mutateAsync({ name, config: { diff --git a/frontend/src/app/runner-config-table.tsx b/frontend/src/app/runner-config-table.tsx index 03063f5b6f..5deff294dd 100644 --- a/frontend/src/app/runner-config-table.tsx +++ b/frontend/src/app/runner-config-table.tsx @@ -1,17 +1,23 @@ import { + faChevronDown, + faChevronRight, faCog, faCogs, faNextjs, faRailway, faTrash, + faTriangleExclamation, faVercel, Icon, } from "@rivet-gg/icons"; import type { Rivet } from "@rivetkit/engine-api-full"; import { Link } from "@tanstack/react-router"; +import { useMemo, useState } from "react"; +import { match, P } from "ts-pattern"; import { Button, DiscreteCopyButton, + Ping, Skeleton, Table, TableBody, @@ -45,6 +51,7 @@ export function RunnerConfigsTable({ + Name Provider Endpoint @@ -55,7 +62,7 @@ export function RunnerConfigsTable({ {!isLoading && !isError && configs?.length === 0 ? ( - + There's no providers matching criteria. @@ -64,7 +71,7 @@ export function RunnerConfigsTable({ ) : null} {isError ? ( - + An error occurred while fetching providers. @@ -89,7 +96,7 @@ export function RunnerConfigsTable({ {!isLoading && hasNextPage ? ( - + + } + /> + + + + + {isExpanded && + groupedConfigs.map((groupedConfig, idx) => ( + + ))} + + ); +} + +function RunnerPoolError({ + error, +}: { + error: Rivet.RunnerPoolError | undefined; +}) { + return match(error) + .with(P.nullish, () => null) + .with(P.string, (errStr) => + match(errStr) + .with( + "internal_error", + () => "Internal error occurred in runner pool", + ) + .with( + "serverless_invalid_base64", + () => "Invalid base64 encoding in serverless response", + ) + .with( + "serverless_stream_ended_early", + () => "Connection terminated unexpectedly", + ) + .otherwise(() => "Unknown runner pool error"), + ) + .with(P.shape({ serverlessHttpError: P.any }), (errObj) => { + const { statusCode, body } = errObj.serverlessHttpError; + const code = statusCode ?? "unknown"; + return body ? `HTTP ${code} error: ${body}` : `HTTP ${code} error`; + }) + .with(P.shape({ serverlessConnectionError: P.any }), (errObj) => { + const message = errObj.serverlessConnectionError?.message; + return message + ? `Connection failed: ${message}` + : "Unable to connect to serverless endpoint"; + }) + .with(P.shape({ serverlessInvalidPayload: P.any }), (errObj) => { + const message = errObj.serverlessInvalidPayload?.message; + return message + ? `Invalid request payload: ${message}` + : "Request payload validation failed"; + }) + .otherwise(() => "Unexpected runner pool error"); +} + +function StatusCell({ + errors, +}: { + errors: Record | string; +}) { + const hasErrors = typeof errors !== "string" ? Object.values(errors).some((err) => err !== undefined) : !!errors; + + if (!hasErrors) { + return ( + + + + ); + } + + return ( + + + {typeof errors !== "string" ? Object.entries(errors).map(([dc, error]) => { + if (!error) return null; + return ( +
+ + +
+ ); + }) : errors} + + } + trigger={ + + + + } + /> +
+ ); +} + +function ProviderRow({ + name, + metadata, + endpoint, + runnerPoolErrors, + datacenters, +}: GroupedConfig & { name: string }) { return ( + {name} - + + - {config.serverless?.url && - config.serverless.url.length > 32 - ? `${config.serverless.url.slice(0, 16)}...${config.serverless.url.slice(-16)}` - : config.serverless?.url} + {endpoint && endpoint.length > 32 + ? `${endpoint.slice(0, 16)}...${endpoint.slice(-16)}` + : endpoint || "-"} } /> - + -
- {config.serverless && - hasMetadataProvider(config.metadata) ? ( + {endpoint && hasMetadataProvider(metadata) ? ( @@ -208,10 +420,20 @@ function Row({ ); } -function getModal(provider: string | undefined) { +function getModal(metadata: unknown) { return "edit-provider-config"; } +function getProviderName(metadata: unknown): string { + if (!metadata || typeof metadata !== "object") { + return "unknown"; + } + if ("provider" in metadata && typeof metadata.provider === "string") { + return metadata.provider; + } + return "unknown"; +} + function Provider({ metadata }: { metadata: unknown }) { if (!metadata || typeof metadata !== "object") { return Unknown; 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 c3acadc145..d8750f66b6 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 @@ -21,6 +21,7 @@ import { createFileRoute, notFound, Link as RouterLink, + useNavigate, } from "@tanstack/react-router"; import { match } from "ts-pattern"; import { HelpDropdown } from "@/app/help-dropdown"; @@ -480,7 +481,7 @@ function ConnectYourFrontend() { } function ProviderDropdown({ children }: { children: React.ReactNode }) { - const navigate = Route.useNavigate(); + const navigate = useNavigate(); return ( {children} @@ -488,67 +489,67 @@ function ProviderDropdown({ children }: { children: React.ReactNode }) { } - onSelect={() => { + onSelect={() => navigate({ to: ".", search: { modal: "connect-vercel" }, - }); - }} + }) + } > Vercel } - onSelect={() => { + onSelect={() => navigate({ to: ".", search: { modal: "connect-railway" }, - }); - }} + }) + } > Railway } - onSelect={() => { + onSelect={() => navigate({ to: ".", search: { modal: "connect-aws" }, - }); - }} + }) + } > AWS ECS } - onSelect={() => { + onSelect={() => navigate({ to: ".", search: { modal: "connect-gcp" }, - }); - }} + }) + } > Google Cloud Run } - onSelect={() => { + onSelect={() => navigate({ to: ".", search: { modal: "connect-hetzner" }, - }); - }} + }) + } > Hetzner } - onSelect={() => { + onSelect={() => navigate({ to: ".", search: { modal: "connect-custom" }, - }); - }} + }) + } > Custom