diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx index 8a402a93f5..92baae2baa 100644 --- a/frontend/src/app/data-providers/engine-data-provider.tsx +++ b/frontend/src/app/data-providers/engine-data-provider.tsx @@ -462,6 +462,43 @@ export const createNamespaceContext = ({ }, }); }, + + envoysListQueryOptions(opts: { + namespace: string; + name?: string; + envoyKey?: string | string[]; + }) { + return infiniteQueryOptions({ + queryKey: [ + { namespace: opts.namespace }, + "envoys", + opts.name, + opts.envoyKey, + ] as any, + initialPageParam: undefined as string | undefined, + enabled: !!opts.namespace, + queryFn: async ({ pageParam }) => { + const data = await client.envoys.list({ + namespace: opts.namespace, + name: opts.name, + limit: RECORDS_PER_PAGE, + envoyKey: + typeof opts.envoyKey === "string" + ? opts.envoyKey + : opts.envoyKey?.join(","), + cursor: pageParam ?? undefined, + }); + return data; + }, + select: (data) => data.pages.flatMap((page) => page.envoys), + getNextPageParam: (lastPage) => { + if (lastPage.envoys.length < RECORDS_PER_PAGE) { + return undefined; + } + return lastPage.pagination.cursor; + }, + }); + }, }; return { @@ -737,6 +774,9 @@ export const createNamespaceContext = ({ }, }); }, + currentNamespaceEnvoyListQueryOptions() { + return dataProvider.envoysListQueryOptions({ namespace }); + }, }; }; diff --git a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx index 293f89e9cf..dcd4a2a8ee 100644 --- a/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-serverfull-frame.tsx @@ -6,9 +6,7 @@ import { useQuery, useSuspenseInfiniteQuery, } from "@tanstack/react-query"; -import confetti from "canvas-confetti"; import { useWatch } from "react-hook-form"; -import { match } from "ts-pattern"; import z from "zod"; import * as ConnectRailwayForm from "@/app/forms/connect-manual-serverfull-form"; import type { DialogContentProps } from "@/components"; @@ -36,17 +34,6 @@ const stepper = defineStepper( title: "Deploy", assist: false, schema: z.object({}), - next: "Next", - }, - { - id: "step-3", - title: "Wait for the Runner to connect", - assist: true, - schema: z.object({ - success: z.boolean().refine((v) => v === true, { - message: "Runner must be connected to proceed", - }), - }), next: "Add", }, ); @@ -147,7 +134,7 @@ function FormStepper({ content={{ "step-1": () => , "step-2": () => , - "step-3": () => , + // "step-3": () => , }} /> ); @@ -192,10 +179,6 @@ function Step2({ provider }: { provider: Provider }) { ); } -function Step3() { - return ; -} - export const useEndpoint = () => { const datacenter = useWatch({ name: "datacenter" }); diff --git a/frontend/src/app/runners-table.tsx b/frontend/src/app/runners-table.tsx index 0484c08fd7..dc3e34136d 100644 --- a/frontend/src/app/runners-table.tsx +++ b/frontend/src/app/runners-table.tsx @@ -6,7 +6,6 @@ import { faSignalAlt2, faSignalAlt3, faSignalAlt4, - faTriangleExclamation, Icon, } from "@rivet-gg/icons"; @@ -35,7 +34,7 @@ interface RunnersTableProps { isError?: boolean; hasNextPage?: boolean; fetchNextPage?: () => void; - runners: Rivet.Runner[]; + runners: (Rivet.Runner | Rivet.Envoy)[]; } export function RunnersTable({ @@ -90,7 +89,11 @@ export function RunnersTable({ {runners?.map((runner) => ( ))} @@ -142,25 +145,74 @@ function RowSkeleton() { ); } -export function Row(runner: Rivet.Runner & { latestVersion?: number }) { +export function Row( + runner: (Rivet.Runner | Rivet.Envoy) & { latestVersion?: number }, +) { + if ("runnerId" in runner) { + // is a runner + return ( + + + + + + + {runner.runnerId.slice(0, 8)} + + } + /> + + + + {runner.name} + + + + + + + + {runner.remainingSlots}/{runner.totalSlots} + + + + {deriveRivetkitVersionFromMetadata(runner.metadata) ?? ( + - + )} + + + + + + + ); + } + return ( - + - {runner.runnerId.slice(0, 8)} + + {runner.envoyKey.slice(0, 8)} } /> - - {runner.name} + + {runner.poolName} @@ -171,9 +223,7 @@ export function Row(runner: Rivet.Runner & { latestVersion?: number }) { /> - - {runner.remainingSlots}/{runner.totalSlots} - + {runner.slots} {deriveRivetkitVersionFromMetadata(runner.metadata) ?? ( @@ -203,7 +253,11 @@ function CreateTs({ createTs }: { createTs: number }) { ); } -function RunnerStatusBadge(runner: Rivet.Runner & { latestVersion?: number }) { +function RunnerStatusBadge( + runner: Pick & { + latestVersion?: number; + }, +) { const now = Date.now(); const isOutdated = runner.latestVersion !== undefined && diff --git a/frontend/src/lib/data.ts b/frontend/src/lib/data.ts index 68f7fb9f2b..8b64a0a606 100644 --- a/frontend/src/lib/data.ts +++ b/frontend/src/lib/data.ts @@ -10,17 +10,36 @@ export function deriveProviderFromMetadata( .parse(metadata)?.provider; } +const rivetkitSchema = z + .object({ version: z.string().optional() }).partial() + +const metadataSchema = z + .object({ + rivetkit: rivetkitSchema.or(z.string().optional().transform((str) => { + try { + if (typeof str !== "string") return undefined; + const parsed = JSON.parse(str); + return rivetkitSchema.parse(parsed); + } catch { + return undefined; + } + })).optional(), + }) + .partial() + .optional() + export function deriveRivetkitVersionFromMetadata( metadata: unknown, ): string | undefined { - return z - .object({ - rivetkit: z - .object({ version: z.string().optional() }) - .partial() - .optional(), - }) - .partial() - .optional() - .parse(metadata)?.rivetkit?.version; + return metadataSchema + .safeParse(metadata).data?.rivetkit?.version; } + +const safeJsonParse = (str: unknown): unknown => { + if (typeof str !== "string") return str; + try { + return JSON.parse(str); + } catch { + return str; + } +} \ No newline at end of file diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/settings.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/settings.tsx index c7545741ab..3ba9ca4e0f 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/settings.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/settings.tsx @@ -53,6 +53,9 @@ export const Route = createFileRoute( context.queryClient.prefetchInfiniteQuery( dataProvider.runnersQueryOptions(), ), + context.queryClient.prefetchInfiniteQuery( + dataProvider.currentNamespaceEnvoyListQueryOptions(), + ), context.queryClient.prefetchInfiniteQuery( dataProvider.datacentersQueryOptions(), ), @@ -190,6 +193,19 @@ function Runners() { refetchInterval: 5000, }); + const { + isLoading: isLoadingEnvoys, + isError: isErrorEnvoys, + data: envoys, + hasNextPage: hasNextEnvoysPage, + fetchNextPage: fetchNextEnvoysPage, + } = useInfiniteQuery({ + ...useEngineCompatDataProvider().currentNamespaceEnvoyListQueryOptions(), + refetchInterval: 5000, + }); + + const allRunners = [...(envoys || []), ...(runners || [])]; + return ( @@ -202,11 +218,18 @@ function Runners() { { + if (hasNextEnvoysPage) { + fetchNextEnvoysPage(); + } + if (hasNextPage) { + fetchNextPage(); + } + }} + hasNextPage={hasNextPage || hasNextEnvoysPage} />