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 ? (