Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions frontend/src/app/changelog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="py-2">
<div className="flex my-2 justify-between items-center">
<div className="flex items-center gap-2">
<div className="bg-white text-background size-8 rounded-full flex items-center justify-center">
<Icon icon={faSparkle} className="m-0" />
</div>
<h4 className="font-bold text-lg text-foreground">
{isNew ? (
<span>New Update</span>
) : (
<span>Latest Update</span>
)}
</h4>
</div>
<Badge variant="outline">
{new Date(published).toLocaleDateString()}
</Badge>
</div>

<a
href={`https://rivet.gg/changelog/${slug}`}
target="_blank"
rel="noreferrer"
className="block"
>
<Picture className="rounded-md border my-4 h-[200px] w-full block overflow-hidden aspect-video">
<PictureFallback>
<Skeleton className="size-full" />
</PictureFallback>
<PictureImage
className="size-full object-cover animate-in fade-in-0 duration-300 fill-mode-forwards"
src={`https://rivet.gg/${images[0].url}`}
width={images[0].width}
height={images[0].height}
alt={"Changelog entry"}
/>
</Picture>

<p className="font-semibold text-sm">{title}</p>

<p className="text-xs mt-1 text-muted-foreground">
{description}{" "}
<span className="text-right text-xs inline gap-1.5 text-foreground items-center">
Read more...
</span>
</p>
</a>
<div className="flex items-end justify-end mt-2">
<div className="flex gap-2 items-center">
<a
className="flex gap-1.5 items-center flex-row-reverse text-right"
href={authors[0].socials.twitter}
>
Comment on lines +83 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The href attribute references authors[0].socials.twitter directly, but according to the schema definition in types.ts, this property is optional. This could lead to broken links if an author doesn't have a Twitter account. Consider adding a fallback:

href={authors[0].socials.twitter || '#'}

Or implement a more robust approach that selects the first available social link from the author's socials object, with a sensible default when none exist.

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

<Avatar className="size-8">
<AvatarFallback>
{authors[0].name[0]}
</AvatarFallback>
<AvatarImage
src={`https://rivet.gg/${authors[0].avatar.url}`}
alt={authors[0].name}
/>
</Avatar>
<div className="ml-2">
<p className="font-semibold text-sm">
{authors[0].name}
</p>
<p className="text-xs text-muted-foreground">
{authors[0].role}
</p>
</div>
</a>
</div>
</div>
</div>
);
}
interface ChangelogProps {
className?: string;
children?: React.ReactNode;
}

export function Changelog({ className, children, ...props }: ChangelogProps) {
const { data } = useSuspenseQuery(changelogQueryOptions());

const [lastChangelog, setLast] = useLocalStorage<string | null>(
"rivet-lastchangelog",
null,
);

const hasNewChangelog = !lastChangelog
? data.length > 0
: data.some(
(entry) => new Date(entry.published) > new Date(lastChangelog),
);

return (
<WithTooltip
delayDuration={0}
contentProps={{ collisionPadding: 8 }}
onOpenChange={(isOpen) => {
if (isOpen) {
setLast(data[0].published);
}
}}
trigger={
<Slot
{...props}
className={cn(
"relative",
!hasNewChangelog && "[&_[data-changelog-ping]]:hidden",
className,
)}
>
{children}
</Slot>
}
content={<ChangelogEntry {...data[0]} isNew={hasNewChangelog} />}
/>
);
}
20 changes: 8 additions & 12 deletions frontend/src/app/data-providers/engine-data-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -487,9 +487,7 @@ export const createNamespaceContext = ({
},
createRunnerConfigMutationOptions(
opts: {
onSuccess?: (
data: Rivet.NamespacesRunnerConfigsUpsertResponse,
) => void;
onSuccess?: (data: Rivet.RunnerConfigsUpsertResponse) => void;
} = {},
) {
return {
Expand All @@ -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;
},
};
Expand All @@ -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,
},
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
cn,
DocsSheet,
type ImperativePanelHandle,
Ping,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
Expand All @@ -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";
Expand Down Expand Up @@ -185,6 +187,26 @@ const Sidebar = ({
__APP_TYPE__ !== "cloud" ? "pb-4" : "",
)}
>
<Changelog>
<Button
className="text-muted-foreground justify-start py-1 h-auto"
variant="ghost"
size="xs"
asChild
>
<a
href="https://rivet.gg/changelog"
target="_blank"
rel="noopener"
>
Whats new?
<Ping
className="relative -right-1"
data-changelog-ping
/>
</a>
</Button>
</Changelog>
<DocsSheet
path={"https://rivet.gg/docs"}
title="Documentation"
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
interface WithTooltipProps extends TooltipPrimitive.TooltipProps {
trigger: React.ReactNode;
content: React.ReactNode;
contentProps?: React.ComponentPropsWithoutRef<typeof TooltipContent>;
}

const WithTooltip = ({ trigger, content, ...rest }: WithTooltipProps) => {
const WithTooltip = ({
trigger,
content,
contentProps,
...rest
}: WithTooltipProps) => {
return (
<Tooltip {...rest}>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipPrimitive.TooltipPortal>
<TooltipContent>{content}</TooltipContent>
<TooltipContent {...contentProps}>{content}</TooltipContent>
</TooltipPrimitive.TooltipPortal>
</Tooltip>
);
Expand Down
25 changes: 24 additions & 1 deletion frontend/src/queries/global.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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: {
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/queries/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Changelog>;
export type ChangelogItem = z.infer<typeof ChangelogItem>;
Loading