From 162941b1adbc14e194cfe8b647f4c54fccf7cf80 Mon Sep 17 00:00:00 2001 From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:02:50 +0100 Subject: [PATCH] feat(frontend): bring back the rivet compute --- frontend/packages/shared-data/src/deploy.ts | 20 +-- .../data-providers/cloud-data-provider.tsx | 54 ++++++++ frontend/src/app/getting-started.tsx | 10 +- frontend/src/components/deployment-logs.tsx | 131 ++++++++++++------ .../projects.$project/ns.$namespace/logs.tsx | 12 +- package.json | 2 +- pnpm-lock.yaml | 20 +-- 7 files changed, 174 insertions(+), 75 deletions(-) diff --git a/frontend/packages/shared-data/src/deploy.ts b/frontend/packages/shared-data/src/deploy.ts index 775452dfa7..8a3109e4f5 100644 --- a/frontend/packages/shared-data/src/deploy.ts +++ b/frontend/packages/shared-data/src/deploy.ts @@ -5,7 +5,7 @@ import { faHetznerH, faKubernetes, faRailway, - // faRivet, + faRivet, faRocket, faServer, faVercel, @@ -24,15 +24,15 @@ export interface DeployOption { } export const deployOptions = [ - // { - // displayName: "Rivet Cloud", - // name: "rivet" as const, - // href: "/docs/connect/rivet-compute", - // description: - // "Deploy to Rivet's managed compute platform", - // icon: faRivet as any, - // badge: "Beta", - // }, + { + displayName: "Rivet Cloud", + name: "rivet" as const, + href: "/docs/connect/rivet-compute", + description: + "Deploy to Rivet's managed compute platform", + icon: faRivet as any, + badge: "Beta", + }, { displayName: "Vercel", name: "vercel" as const, diff --git a/frontend/src/app/data-providers/cloud-data-provider.tsx b/frontend/src/app/data-providers/cloud-data-provider.tsx index 3aca5fd4fc..7b6ce932e1 100644 --- a/frontend/src/app/data-providers/cloud-data-provider.tsx +++ b/frontend/src/app/data-providers/cloud-data-provider.tsx @@ -91,6 +91,45 @@ export const createGlobalContext = ({ clerk }: { clerk: Clerk }) => { }, }); }, + logsHistoryInfiniteQueryOptions(opts: { + organization: string; + project: string; + namespace: string; + pool: string; + contains?: string; + region?: string; + }) { + return infiniteQueryOptions({ + queryKey: [opts, "logs-history"] as const, + queryFn: async ({ pageParam }) => { + const items = await client.managedPools.getLogsHistory( + opts.project, + opts.namespace, + opts.pool, + { + before: pageParam ?? undefined, + limit: 500, + contains: opts.contains || undefined, + region: opts.region || undefined, + org: opts.organization, + }, + ); + // API returns newest-first; reverse page to oldest-first. + return items.reverse(); + }, + initialPageParam: undefined as string | undefined, + // Only offer a previous page if the first (oldest) fetched page was full. + getPreviousPageParam: (firstPage) => + firstPage.length >= 500 ? firstPage[0].timestamp : undefined, + getNextPageParam: () => undefined, + select: (data) => ({ + pages: data.pages, + pageParams: data.pageParams, + logs: data.pages.flat(), + }), + staleTime: Number.POSITIVE_INFINITY, + }); + }, managedPoolsQueryOptions(opts: { organization: string; project: string; @@ -1135,6 +1174,21 @@ export const createNamespaceContext = ({ }); }, + currentNamespaceLogsHistoryInfiniteQueryOptions(opts: { + pool: string; + contains?: string; + region?: string; + }) { + return parent.logsHistoryInfiniteQueryOptions({ + organization: parent.organization, + project: parent.project, + namespace, + pool: opts.pool, + contains: opts.contains, + region: opts.region, + }); + }, + upsertCurrentNamespaceManagedPoolMutationOptions() { return mutationOptions({ mutationKey: diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx index 19c6c8ae91..5c48c2b5a7 100644 --- a/frontend/src/app/getting-started.tsx +++ b/frontend/src/app/getting-started.tsx @@ -379,15 +379,15 @@ function ProviderSetup() { control={control} name="provider" render={({ field }) => { - // const rivetCompute = filteredOptions.find( - // (o) => o.name === "rivet", - // ); + const rivetCompute = filteredOptions.find( + (o) => o.name === "rivet", + ); const rest = filteredOptions.filter( (o) => o.name !== "rivet", ); return (
- {/* {rivetCompute ? ( + {rivetCompute ? ( - ) : null} */} + ) : null}
{rest.map((option) => ( ; + logsRef?: React.MutableRefObject; } -interface LogRowProps { +interface LogRowData { className?: string; - entry: RivetSse.LogStreamEvent.Log; + entry?: Rivet.LogHistoryResponseItem; + isSentinel?: boolean; + isLoadingMore?: boolean; } -function LogRow({ entry, ...props }: LogRowProps) { +function LogRow({ entry, isSentinel, isLoadingMore, ...props }: LogRowData) { + if (isSentinel) { + return ( +
+ {isLoadingMore ? "Loading older logs…" : "Scroll to top to load older logs"} +
+ ); + } + + if (!entry) return null; + return (
- {entry.data.timestamp} + {entry.timestamp} - {entry.data.region ? ( + {entry.region ? ( - [{entry.data.region}] + [{entry.region}] ) : null} - +
@@ -59,38 +80,52 @@ function LogRow({ entry, ...props }: LogRowProps) { } export function DeploymentLogs({ - namespace, pool, filter, region, paused, logsRef, }: DeploymentLogsProps) { - const { project } = useCloudNamespaceDataProvider(); - const { logs, isLoading, error } = useDeploymentLogsStream({ - project, - namespace, - pool, - filter, - region, - paused, - }); + const { logs, isLoading, error, streamError, isLoadingMore, hasMore, loadMoreHistory } = + useDeploymentLogsStream({ pool, filter, region, paused }); const viewportRef = useRef(null); const virtualizerRef = useRef>(null); const [follow, setFollow] = useState(true); + // Track the log count before a load-more so we can restore scroll position. + const prevLogCountRef = useRef(0); + + // When hasMore, index 0 is the sentinel row; real logs start at index 1. + const sentinelOffset = hasMore ? 1 : 0; + const totalCount = logs.length + sentinelOffset; useEffect(() => { if (follow && !isLoading && virtualizerRef.current && logs.length > 0) { // https://github.com/TanStack/virtual/issues/537 const rafId = requestAnimationFrame(() => { - virtualizerRef.current?.scrollToIndex(logs.length - 1, { + virtualizerRef.current?.scrollToIndex(totalCount - 1, { align: "end", }); }); return () => cancelAnimationFrame(rafId); } - }, [logs.length, follow, isLoading]); + }, [totalCount, logs.length, follow, isLoading]); + + // After prepending older history, scroll to restore the previously-first row. + const wasLoadingMoreRef = useRef(false); + useEffect(() => { + if (wasLoadingMoreRef.current && !isLoadingMore && logs.length > prevLogCountRef.current) { + const addedCount = logs.length - prevLogCountRef.current; + const rafId = requestAnimationFrame(() => { + // +1 to skip sentinel row at index 0. + virtualizerRef.current?.scrollToIndex(addedCount + sentinelOffset, { + align: "start", + }); + }); + return () => cancelAnimationFrame(rafId); + } + wasLoadingMoreRef.current = isLoadingMore; + }, [isLoadingMore, logs.length, sentinelOffset]); useEffect(() => { if (logsRef) { @@ -101,15 +136,20 @@ export function DeploymentLogs({ const handleScrollChange = useCallback( (instance: Virtualizer) => { const isAtBottom = - (instance.range?.endIndex ?? 0) >= logs.length - 1; + (instance.range?.endIndex ?? 0) >= totalCount - 1; if (isAtBottom) { return setFollow(true); } if (instance.scrollDirection === "backward") { - return setFollow(false); + setFollow(false); + // Load more when the sentinel row comes into view. + if ((instance.range?.startIndex ?? 1) === 0 && hasMore && !isLoadingMore) { + prevLogCountRef.current = logs.length; + loadMoreHistory(); + } } }, - [logs.length], + [totalCount, logs.length, hasMore, isLoadingMore, loadMoreHistory], ); if (isLoading) { @@ -119,9 +159,9 @@ export function DeploymentLogs({ className="w-full h-full" viewportProps={{ className: "p-2" }} > - {Array.from({ length: 40 }).map((_, i) => ( + {SKELETON_KEYS.map((key) => ( ))} @@ -158,22 +198,31 @@ export function DeploymentLogs({ } return ( -
- +
+ {streamError ? ( +
+ + Stream error: {streamError} +
+ ) : null} + virtualizerRef={virtualizerRef} viewportRef={viewportRef} onChange={handleScrollChange} - count={logs.length} + count={totalCount} estimateSize={() => 24} - className="w-full h-full" + className="w-full flex-1 min-h-0" scrollerProps={{ className: "w-full", }} viewportProps={{}} - getRowData={(index) => ({ - index: index, - })} - row={(props) => } + getRowData={(index) => { + if (hasMore && index === 0) { + return { isSentinel: true, isLoadingMore }; + } + return { entry: logs[index - sentinelOffset] }; + }} + row={LogRow} />
); diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/logs.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/logs.tsx index 6eba5f42f9..d3dd3051a8 100644 --- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/logs.tsx +++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace/logs.tsx @@ -1,4 +1,4 @@ -import type { RivetSse } from "@rivet-gg/cloud"; +import type { Rivet } from "@rivet-gg/cloud"; import { faCopy, faDownload, @@ -9,7 +9,7 @@ import { } from "@rivet-gg/icons"; import { useInfiniteQuery, useSuspenseQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; -import { startTransition, useCallback, useMemo, useRef, useState } from "react"; +import { startTransition, useCallback, useRef, useState } from "react"; import { HelpDropdown } from "@/app/help-dropdown"; import { Content } from "@/app/layout"; import { SidebarToggle } from "@/app/sidebar-toggle"; @@ -61,15 +61,12 @@ function RouteComponent() { const [search, setSearch] = useState(""); const [isPaused, setIsPaused] = useState(false); const [region, setRegion] = useState("all"); - const logsRef = useRef([]); + const logsRef = useRef([]); const getLogsText = useCallback( () => logsRef.current - .map( - (e) => - `${e.data.timestamp}\t${e.data.region}\t${e.data.message}`, - ) + .map((e) => `${e.timestamp}\t${e.region}\t${e.message}`) .join("\n"), [], ); @@ -172,7 +169,6 @@ function RouteComponent() { {pool ? (