From 8808a6935a121f7915b2ef5b47009fbc680c7047 Mon Sep 17 00:00:00 2001
From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com>
Date: Thu, 18 Sep 2025 22:50:54 +0200
Subject: [PATCH] featI(cloud): add changelog
---
frontend/src/app/changelog.tsx | 153 ++++++++++++++++++
.../data-providers/engine-data-provider.tsx | 20 +--
frontend/src/app/layout.tsx | 22 +++
frontend/src/components/ui/tooltip.tsx | 10 +-
frontend/src/queries/global.ts | 25 ++-
frontend/src/queries/types.ts | 27 ++++
6 files changed, 242 insertions(+), 15 deletions(-)
create mode 100644 frontend/src/app/changelog.tsx
create mode 100644 frontend/src/queries/types.ts
diff --git a/frontend/src/app/changelog.tsx b/frontend/src/app/changelog.tsx
new file mode 100644
index 0000000000..454edcb18f
--- /dev/null
+++ b/frontend/src/app/changelog.tsx
@@ -0,0 +1,153 @@
+import { faSparkle, Icon } from "@rivet-gg/icons";
+import { useSuspenseQuery } from "@tanstack/react-query";
+import { useLocalStorage } from "usehooks-ts";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+ Badge,
+ cn,
+ Picture,
+ PictureFallback,
+ PictureImage,
+ Skeleton,
+ Slot,
+ WithTooltip,
+} from "@/components";
+import { changelogQueryOptions } from "@/queries/global";
+import type { ChangelogItem } from "@/queries/types";
+
+interface ChangelogEntryProps extends ChangelogItem {
+ isNew?: boolean;
+}
+
+export function ChangelogEntry({
+ published,
+ images,
+ title,
+ description,
+ slug,
+ authors,
+ isNew,
+}: ChangelogEntryProps) {
+ return (
+
+ );
+}
+interface ChangelogProps {
+ className?: string;
+ children?: React.ReactNode;
+}
+
+export function Changelog({ className, children, ...props }: ChangelogProps) {
+ const { data } = useSuspenseQuery(changelogQueryOptions());
+
+ const [lastChangelog, setLast] = useLocalStorage(
+ "rivet-lastchangelog",
+ null,
+ );
+
+ const hasNewChangelog = !lastChangelog
+ ? data.length > 0
+ : data.some(
+ (entry) => new Date(entry.published) > new Date(lastChangelog),
+ );
+
+ return (
+ {
+ if (isOpen) {
+ setLast(data[0].published);
+ }
+ }}
+ trigger={
+
+ {children}
+
+ }
+ content={}
+ />
+ );
+}
diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx
index 6ea9a862ee..1a8e548dcd 100644
--- a/frontend/src/app/data-providers/engine-data-provider.tsx
+++ b/frontend/src/app/data-providers/engine-data-provider.tsx
@@ -487,9 +487,7 @@ export const createNamespaceContext = ({
},
createRunnerConfigMutationOptions(
opts: {
- onSuccess?: (
- data: Rivet.NamespacesRunnerConfigsUpsertResponse,
- ) => void;
+ onSuccess?: (data: Rivet.RunnerConfigsUpsertResponse) => void;
} = {},
) {
return {
@@ -500,14 +498,12 @@ export const createNamespaceContext = ({
config,
}: {
name: string;
- config: Rivet.NamespacesRunnerConfig;
+ config: Rivet.RunnerConfig;
}) => {
- const response =
- await client.namespacesRunnerConfigs.upsert(
- namespaceId,
- name,
- config,
- );
+ const response = await client.runnerConfigs.upsert(name, {
+ namespace,
+ ...config,
+ });
return response;
},
};
@@ -517,9 +513,9 @@ export const createNamespaceContext = ({
queryKey: [{ namespace }, "runners", "configs"],
initialPageParam: undefined as string | undefined,
queryFn: async ({ signal: abortSignal, pageParam }) => {
- const response = await client.namespacesRunnerConfigs.list(
- namespace,
+ const response = await client.runnerConfigs.list(
{
+ namespace,
cursor: pageParam ?? undefined,
limit: RECORDS_PER_PAGE,
},
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 8e3de130c1..7ebef7246e 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -34,6 +34,7 @@ import {
cn,
DocsSheet,
type ImperativePanelHandle,
+ Ping,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
@@ -44,6 +45,7 @@ import { useInspectorDataProvider } from "@/components/actors";
import type { HeaderLinkProps } from "@/components/header/header-link";
import { ensureTrailingSlash } from "@/lib/utils";
import { ActorBuildsList } from "./actor-builds-list";
+import { Changelog } from "./changelog";
import { ContextSwitcher } from "./context-switcher";
import { useInspectorCredentials } from "./credentials-context";
import { NamespaceSelect } from "./namespace-select";
@@ -185,6 +187,26 @@ const Sidebar = ({
__APP_TYPE__ !== "cloud" ? "pb-4" : "",
)}
>
+
+
+
;
}
-const WithTooltip = ({ trigger, content, ...rest }: WithTooltipProps) => {
+const WithTooltip = ({
+ trigger,
+ content,
+ contentProps,
+ ...rest
+}: WithTooltipProps) => {
return (
{trigger}
- {content}
+ {content}
);
diff --git a/frontend/src/queries/global.ts b/frontend/src/queries/global.ts
index 7c4dfe4573..a4f137134c 100644
--- a/frontend/src/queries/global.ts
+++ b/frontend/src/queries/global.ts
@@ -1,6 +1,12 @@
-import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
+import {
+ MutationCache,
+ QueryCache,
+ QueryClient,
+ queryOptions,
+} from "@tanstack/react-query";
import { toast } from "@/components";
import { modal } from "@/utils/modal-utils";
+import { Changelog } from "./types";
const queryCache = new QueryCache({
onError(error, query) {
@@ -26,6 +32,23 @@ const mutationCache = new MutationCache({
},
});
+export const changelogQueryOptions = () => {
+ return queryOptions({
+ queryKey: ["changelog", __APP_BUILD_ID__],
+ staleTime: 1 * 60 * 60 * 1000, // 1 hour
+ queryFn: async () => {
+ const response = await fetch(
+ "https://rivet-site.vercel.app/changelog.json",
+ );
+ if (!response.ok) {
+ throw new Error("Failed to fetch changelog");
+ }
+ const result = Changelog.parse(await response.json());
+ return result;
+ },
+ });
+};
+
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
diff --git a/frontend/src/queries/types.ts b/frontend/src/queries/types.ts
new file mode 100644
index 0000000000..3dc37c341f
--- /dev/null
+++ b/frontend/src/queries/types.ts
@@ -0,0 +1,27 @@
+import { z } from "zod";
+
+export const ChangelogItem = z.object({
+ published: z.string(),
+ images: z.array(
+ z.object({ url: z.string(), width: z.number(), height: z.number() }),
+ ),
+ title: z.string(),
+ description: z.string(),
+ slug: z.string(),
+ authors: z.array(
+ z.object({
+ name: z.string(),
+ role: z.string(),
+ avatar: z.object({ url: z.string() }),
+ socials: z.object({
+ twitter: z.string().optional(),
+ github: z.string().optional(),
+ bluesky: z.string().optional(),
+ }),
+ }),
+ ),
+});
+export const Changelog = z.array(ChangelogItem);
+
+export type Changelog = z.infer;
+export type ChangelogItem = z.infer;