diff --git a/frontend/package.json b/frontend/package.json
index ca8f70eb3d..9b79c0eb68 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,7 +9,7 @@
"dev:engine": "vite --config vite.engine.config.ts",
"dev:cloud": "vite --config vite.cloud.config.ts",
"ts-check": "tsc --noEmit",
- "build": "echo 'Please use build:engine or build:inspector' && exit 1",
+ "build": "echo 'Please use build:engine or build:inspector or build:cloud' && exit 1",
"build:inspector": "vite build --mode=production --config vite.inspector.config.ts",
"build:engine": "vite build --mode=production --config vite.engine.config.ts",
"build:cloud": "vite build --mode=production --config vite.cloud.config.ts",
@@ -21,7 +21,7 @@
"@clerk/clerk-js": "^5.92.1",
"@clerk/clerk-react": "^5.47.0",
"@clerk/elements": "^0.23.63",
- "@clerk/shared": "3.25.0",
+ "@clerk/shared": "*",
"@clerk/themes": "^2.4.18",
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.8.1",
diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx
index 8bc577658f..e9681c73a1 100644
--- a/frontend/src/app.tsx
+++ b/frontend/src/app.tsx
@@ -79,7 +79,7 @@ function CloudApp() {
colorModalBackdrop: "rgb(0 0 0 / 0.8)",
},
}}
- publishableKey={cloudEnv().VITE_CLERK_PUBLISHABLE_KEY}
+ publishableKey={cloudEnv().VITE_APP_CLERK_PUBLISHABLE_KEY}
>
diff --git a/frontend/src/app/actor-builds-list.tsx b/frontend/src/app/actor-builds-list.tsx
index 88c58dc05d..86f75005e0 100644
--- a/frontend/src/app/actor-builds-list.tsx
+++ b/frontend/src/app/actor-builds-list.tsx
@@ -1,8 +1,4 @@
-import {
- // @ts-expect-error
- faActorsBorderless,
- Icon,
-} from "@rivet-gg/icons";
+import { faActorsBorderless, Icon } from "@rivet-gg/icons";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { Fragment } from "react";
@@ -22,8 +18,8 @@ export function ActorBuildsList() {
{data?.length === 0 ? (
-
- No instances found.
+
+ Connect RivetKit to see instances.
) : null}
{data?.map((build) => (
@@ -84,3 +80,19 @@ export function ActorBuildsList() {
);
}
+
+export function ActorBuildsListSkeleton() {
+ return (
+
+
+ {Array(RECORDS_PER_PAGE)
+ .fill(null)
+ .map((_, i) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/app/context-switcher.tsx b/frontend/src/app/context-switcher.tsx
index 21a8a76294..0e8e9a3a2f 100644
--- a/frontend/src/app/context-switcher.tsx
+++ b/frontend/src/app/context-switcher.tsx
@@ -24,6 +24,12 @@ import { VisibilitySensor } from "@/components/visibility-sensor";
export function ContextSwitcher() {
const [isOpen, setIsOpen] = useState(false);
+ const match = useContextSwitchMatch();
+
+ if (!match) {
+ return null;
+ }
+
return (
<>
@@ -47,45 +53,69 @@ export function ContextSwitcher() {
);
}
-function Breadcrumbs() {
+const useContextSwitchMatch = ():
+ | {
+ project: string;
+ namespace: string;
+ organization: string;
+ }
+ | { organization: string; project: string }
+ | false => {
const match = useMatchRoute();
const matchNamespace = match({
to: "/orgs/$organization/projects/$project/ns/$namespace",
fuzzy: true,
});
+
if (matchNamespace) {
+ return matchNamespace;
+ }
+
+ const matchProject = match({
+ to: "/orgs/$organization/projects/$project",
+ fuzzy: true,
+ });
+
+ if (matchProject) {
+ return matchProject;
+ }
+
+ return false;
+};
+
+function Breadcrumbs() {
+ const match = useContextSwitchMatch();
+
+ if (match && "project" in match && "namespace" in match) {
return (
);
}
- const matchProject = match({
- to: "/orgs/$organization/projects/$project",
- fuzzy: true,
- });
-
- if (matchProject) {
+ if (match && "project" in match) {
return (
<>
-
+
>
);
}
+
+ return null;
}
function ProjectBreadcrumb({
@@ -102,7 +132,7 @@ function ProjectBreadcrumb({
return ;
}
- return {data?.name};
+ return {data?.displayName};
}
function NamespaceBreadcrumb({
@@ -124,7 +154,7 @@ function NamespaceBreadcrumb({
return ;
}
- return {data?.name};
+ return {data?.displayName};
}
function Content({ onClose }: { onClose?: () => void }) {
diff --git a/frontend/src/app/data-providers/cloud-data-provider.tsx b/frontend/src/app/data-providers/cloud-data-provider.tsx
index cf09e06c33..4633cef2fe 100644
--- a/frontend/src/app/data-providers/cloud-data-provider.tsx
+++ b/frontend/src/app/data-providers/cloud-data-provider.tsx
@@ -1,5 +1,6 @@
import type { Clerk } from "@clerk/clerk-js";
import { type Rivet, RivetClient } from "@rivet-gg/cloud";
+import { type FetchFunction, fetcher } from "@rivetkit/engine-api-full/core";
import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query";
import { cloudEnv } from "@/lib/env";
import { RECORDS_PER_PAGE } from "./default-data-provider";
@@ -17,6 +18,14 @@ function createClient({ clerk }: { clerk: Clerk }) {
token: async () => {
return (await clerk.session?.getToken()) || "";
},
+ fetcher: async (args) => {
+ if (args.headers) {
+ delete args.headers["X-Fern-Language"];
+ delete args.headers["X-Fern-Runtime"];
+ delete args.headers["X-Fern-Runtime-Version"];
+ }
+ return await fetcher(args);
+ },
});
}
diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx
index 1a8e548dcd..2f4642fc0a 100644
--- a/frontend/src/app/data-providers/engine-data-provider.tsx
+++ b/frontend/src/app/data-providers/engine-data-provider.tsx
@@ -8,13 +8,13 @@ import {
} from "@/components/actors";
import { engineEnv } from "@/lib/env";
import { convertStringToId } from "@/lib/utils";
+import { noThrow, shouldRetryAllExpect403 } from "@/queries/utils";
import {
ActorQueryOptionsSchema,
createDefaultGlobalContext,
type DefaultDataProvider,
RECORDS_PER_PAGE,
} from "./default-data-provider";
-import { noThrow, shouldRetryAllExpect403 } from "./utils";
export type CreateNamespace = {
displayName: string;
diff --git a/frontend/src/app/dialogs/billing-frame.tsx b/frontend/src/app/dialogs/billing-frame.tsx
index f5995191a5..e8f42fd018 100644
--- a/frontend/src/app/dialogs/billing-frame.tsx
+++ b/frontend/src/app/dialogs/billing-frame.tsx
@@ -1,3 +1,4 @@
+import { Rivet } from "@rivet-gg/cloud";
import {
useMutation,
useQuery,
@@ -101,61 +102,135 @@ export default function BillingFrameContent() {
-
{
+ const config = getConfig(plan, billing);
+ return (
+ {
+ if (billing.futurePlan === plan) {
+ return mutate({
+ plan: Rivet.BillingPlan.Free,
+ __from: plan,
+ });
+ }
+ mutate({ plan, __from: plan });
+ },
+ }}
+ />
+ );
+ })}
+ mutate({ plan: "community" }),
- disabled:
- billing?.activePlan === "free" ||
- !billing?.activePlan ||
- !billing?.canChangePlan,
- }}
- />
- {
- if (billing?.activePlan === "pro") {
- return mutate({ plan: "free" });
- }
- return mutate({ plan: "pro" });
+ window.open(
+ "https://www.rivet.dev/sales",
+ "_blank",
+ );
},
- disabled: !billing?.canChangePlan,
- ...(billing?.activePlan === "pro" &&
- billing?.futurePlan !== "free"
- ? { children: "Cancel" }
- : {}),
}}
/>
- {
- if (billing?.activePlan === "team") {
- return mutate({ plan: "free" });
- }
- return mutate({ plan: "team" });
- },
- disabled: !billing?.canChangePlan,
- ...(billing?.activePlan === "team" &&
- billing?.futurePlan !== "free"
- ? { children: "Cancel" }
- : {}),
- }}
- />
-
>
);
}
+function isCurrent(
+ plan: Rivet.BillingPlan,
+ data: Rivet.BillingDetailsResponse.Billing,
+) {
+ return (
+ plan === data.activePlan ||
+ (plan === Rivet.BillingPlan.Free && !data.activePlan)
+ );
+}
+
+function getConfig(
+ plan: Rivet.BillingPlan,
+ billing: Rivet.BillingDetailsResponse.Billing | undefined,
+) {
+ return {
+ current: isCurrent(plan, billing),
+ buttonProps: {
+ children: buttonText(plan, billing),
+ variant: buttonVariant(plan, billing),
+ disabled: !billing?.canChangePlan || buttonDisabled(plan, billing),
+ },
+ };
+}
+
+function buttonVariant(
+ plan: Rivet.BillingPlan,
+ data: Rivet.BillingDetailsResponse.Billing,
+) {
+ if (plan === data.activePlan && data.futurePlan !== data.activePlan)
+ return "default";
+ if (plan === data.futurePlan && data.futurePlan !== data.activePlan)
+ return "secondary";
+
+ if (comparePlans(plan, data.futurePlan) > 0) return "default";
+ return "secondary";
+}
+
+function buttonDisabled(
+ plan: Rivet.BillingPlan,
+ data: Rivet.BillingDetailsResponse.Billing,
+) {
+ return plan === data.futurePlan && data.futurePlan !== data.activePlan;
+}
+
+function buttonText(
+ plan: Rivet.BillingPlan,
+ data: Rivet.BillingDetailsResponse.Billing,
+) {
+ if (plan === data.activePlan && data.futurePlan !== data.activePlan)
+ return <>Resubscribe>;
+ if (plan === data.futurePlan && data.futurePlan !== data.activePlan)
+ return (
+ <>
+ Downgrades on{" "}
+ {new Date(data.currentPeriodEnd).toLocaleDateString(undefined, {
+ month: "short",
+ day: "numeric",
+ })}
+ >
+ );
+ if (plan === data.activePlan) return "Cancel";
+ return comparePlans(plan, data.futurePlan) > 0 ? "Upgrade" : "Downgrade";
+}
+
+export function comparePlans(
+ a: Rivet.BillingPlan,
+ b: Rivet.BillingPlan,
+): number {
+ const plans = [
+ Rivet.BillingPlan.Free,
+ Rivet.BillingPlan.Pro,
+ Rivet.BillingPlan.Team,
+ Rivet.BillingPlan.Enterprise,
+ ];
+
+ const tierA = plans.indexOf(a);
+ const tierB = plans.indexOf(b);
+
+ if (tierA > tierB) return 1;
+ if (tierA < tierB) return -1;
+ return 0;
+}
+
function CurrentPlan({ plan }: { plan?: string }) {
if (!plan || plan === "free") return <>Free>;
if (plan === "pro") return <>Hobby>;
diff --git a/frontend/src/app/dialogs/create-project-frame.tsx b/frontend/src/app/dialogs/create-project-frame.tsx
index a610bd89f5..1ed4f7bf82 100644
--- a/frontend/src/app/dialogs/create-project-frame.tsx
+++ b/frontend/src/app/dialogs/create-project-frame.tsx
@@ -39,10 +39,9 @@ export default function CreateProjectFrameContent() {
onSubmit={async (values) => {
await mutateAsync({
displayName: values.name,
- nameId: values.slug || convertStringToId(values.name),
});
}}
- defaultValues={{ name: "", slug: "" }}
+ defaultValues={{ name: "" }}
>
Create Project
@@ -50,7 +49,6 @@ export default function CreateProjectFrameContent() {
-
diff --git a/frontend/src/app/forms/create-project-form.tsx b/frontend/src/app/forms/create-project-form.tsx
index 8474e99b9b..1a940b1279 100644
--- a/frontend/src/app/forms/create-project-form.tsx
+++ b/frontend/src/app/forms/create-project-form.tsx
@@ -14,11 +14,10 @@ import { convertStringToId } from "@/lib/utils";
export const formSchema = z.object({
name: z
.string()
- .max(16)
+ .max(16, "Name must be at most 16 characters long")
.refine((value) => value.trim() !== "" && value.trim() === value, {
message: "Name cannot be empty or contain whitespaces",
}),
- slug: z.string().max(16).optional(),
});
export type FormValues = z.infer;
@@ -53,39 +52,3 @@ export const Name = ({ className }: { className?: string }) => {
/>
);
};
-
-export const Slug = ({ className }: { className?: string }) => {
- const { control, watch } = useFormContext();
-
- const name = watch("name");
-
- return (
- (
-
- Slug
-
- {
- const value = convertStringToId(
- event.target.value,
- );
- field.onChange({ target: { value } });
- }}
- />
-
-
-
- )}
- />
- );
-};
diff --git a/frontend/src/app/help-dropdown.tsx b/frontend/src/app/help-dropdown.tsx
index f624bed1bf..0a96551c96 100644
--- a/frontend/src/app/help-dropdown.tsx
+++ b/frontend/src/app/help-dropdown.tsx
@@ -3,8 +3,10 @@ import {
faComments,
faDiscord,
faGithub,
+ faMessageSmile,
Icon,
} from "@rivet-gg/icons";
+import { useNavigate } from "@tanstack/react-router";
import type { ReactNode } from "react";
import {
DropdownMenu,
@@ -14,6 +16,7 @@ import {
} from "@/components";
export const HelpDropdown = ({ children }: { children: ReactNode }) => {
+ const navigate = useNavigate();
return (
{children}
@@ -45,6 +48,17 @@ export const HelpDropdown = ({ children }: { children: ReactNode }) => {
>
Documentation
+ }
+ onSelect={() => {
+ navigate({
+ to: ".",
+ search: (old) => ({ ...old, modal: "feedback" }),
+ });
+ }}
+ >
+ Feedback
+
{__APP_TYPE__ === "cloud" ? (
}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 496e90dd66..e2a0faea92 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -208,15 +208,37 @@ const Sidebar = ({
-
-
-
+ {match(__APP_TYPE__)
+ .with("cloud", () => (
+
+ ))
+ .otherwise(() => (
+
+ ))}
-