Skip to content
Draft
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
16 changes: 15 additions & 1 deletion frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,21 @@ function CloudApp() {
return (
<ClerkProvider
Clerk={clerk}
appearance={{ baseTheme: dark }}
appearance={{
baseTheme: dark,
variables: {
colorPrimary: "hsl(var(--primary))",
colorPrimaryForeground: "hsl(var(--primary-foreground))",
colorTextOnPrimaryBackground:
"hsl(var(--primary-foreground))",
colorBackground: "hsl(var(--background))",
colorInput: "hsl(var(--input))",
colorText: "hsl(var(--text))",
colorTextSecondary: "hsl(var(--muted-foreground))",
borderRadius: "var(--radius)",
colorModalBackdrop: "rgb(0 0 0 / 0.8)",
},
}}
publishableKey={cloudEnv().VITE_CLERK_PUBLISHABLE_KEY}
>
<RouterProvider router={router} />
Expand Down
156 changes: 156 additions & 0 deletions frontend/src/app/billing/plan-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { faCheck, faPlus, Icon, type IconProp } from "@rivet-gg/icons";
import type { ReactNode } from "react";
import { Button, cn } from "@/components";

type PlanCardProps = {
title: string;
price: string;
features: { icon: IconProp; label: ReactNode }[];
usageBased?: boolean;
custom?: boolean;
current?: boolean;
buttonProps?: React.ComponentProps<typeof Button>;
} & React.ComponentProps<"div">;

function PlanCard({
title,
price,
features,
usageBased,
current,
custom,
className,
buttonProps,
...props
}: PlanCardProps) {
return (
<div
className={cn(
"border rounded-lg p-6 h-full flex flex-col hover:bg-secondary/20 transition-colors",
current && "border-primary",
className,
)}
{...props}
>
<h3 className="text-lg font-medium mb-2">{title}</h3>
<div className="min-h-24">
{usageBased ? (
<p className="text-xs text-muted-foreground">From</p>
) : null}
<p className="">
<span className="text-4xl font-bold">{price}</span>
{custom ? null : (
<span className="text-muted-foreground ml-1">/mo</span>
)}
</p>
{usageBased ? (
<p className="text-sm text-muted-foreground">+ Usage</p>
) : null}
</div>
<div className="text-sm text-primary-foreground border-t pt-2 flex-1">
<p>Includes:</p>
<ul className="text-muted-foreground mt-2 space-y-1">
{features?.map((feature, index) => (
<li key={feature.label}>
<Icon icon={feature.icon} /> {feature.label}
</li>
))}
</ul>
</div>
{current ? (
<Button
variant="secondary"
className="w-full mt-4"
children="Current Plan"
{...buttonProps}
>
</Button>
) : (
<Button className="w-full mt-4" children={<>{custom ? "Contact Us" : "Upgrade"}</>} {...buttonProps}/>
)}
</div>
);
}

export const CommunityPlan = (props: Partial<PlanCardProps>) => {
return (
<PlanCard
title="Free"
price="$0"
features={[
{ icon: faCheck, label: "5GB Limit" },
{ icon: faCheck, label: "5 Million Writes /mo" },
{ icon: faCheck, label: "200 Million Reads /mo" },
{ icon: faCheck, label: "Community Support" },
]}
{...props}
/>
);
};

export const ProPlan = (props: Partial<PlanCardProps>) => {
return (
<PlanCard
title="Hobby"
price="$5"
usageBased
features={[
{
icon: faPlus,
label: "20 Billion Read /mo",
},
{
icon: faPlus,
label: "50 Million Read /mo",
},
{
icon: faPlus,
label: "5GB Storage",
},
{ icon: faCheck, label: "Unlimited Seats" },
{ icon: faCheck, label: "Email Support" },
]}
{...props}
/>
);
};

export const TeamPlan = (props: Partial<PlanCardProps>) => {
return (
<PlanCard
title="Team"
price="$200"
usageBased
features={[
{ icon: faPlus, label: "25 Billion Reads /mo" },
{ icon: faPlus, label: "50 Million Writes /mo" },
{ icon: faPlus, label: "5GB Storage" },
{ icon: faCheck, label: "Unlimited Seats" },
{ icon: faCheck, label: "MFA" },
{ icon: faCheck, label: "Slack Support" },
]}
{...props}
/>
);
};

export const EnterprisePlan = (props: Partial<PlanCardProps>) => {
return (
<PlanCard
title="Enterprise"
price="Custom"
custom
features={[
{ icon: faCheck, label: "Everything in Team" },
{ icon: faCheck, label: "Priority Support" },
{ icon: faCheck, label: "SLA" },
{ icon: faCheck, label: "OIDC SSO provider" },
{ icon: faCheck, label: "On-Prem Deployment" },
{ icon: faCheck, label: "Audit logs" },
{ icon: faCheck, label: "Custom Roles" },
{ icon: faCheck, label: "Device Tracking" },
]}
{...props}
/>
);
};
1 change: 1 addition & 0 deletions frontend/src/app/context-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function Breadcrumbs() {

const matchProject = match({
to: "/orgs/$organization/projects/$project",
fuzzy: true,
});

if (matchProject) {
Expand Down
43 changes: 40 additions & 3 deletions frontend/src/app/data-providers/cloud-data-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const createOrganizationContext = ({
const response = await client.projects.create({
displayName: data.displayName,
name: data.nameId,
org: organization,
organizationId: organization,
});

return response;
Expand Down Expand Up @@ -236,10 +236,22 @@ export const createProjectContext = ({
displayName: data.displayName,
org: organization,
});
return response.namespace;
return {
id: response.namespace.id,
name: response.namespace.name,
displayName: response.namespace.displayName,
createdAt: new Date(
response.namespace.createdAt,
).toISOString(),
};
},
};
},
currentProjectQueryOptions: () => {
return parent.currentOrgProjectQueryOptions({
project,
});
},
currentProjectNamespacesQueryOptions: () => {
return parent.orgProjectNamespacesQueryOptions({
organization,
Expand All @@ -258,6 +270,31 @@ export const createProjectContext = ({
namespace: opts.namespace,
});
},
currentProjectBillingDetailsQueryOptions() {
return queryOptions({
queryKey: [{ organization, project }, "billing-details"],
queryFn: async ({ signal: abortSignal }) => {
const response = await client.billing.details(
project,
{org: organization },
{ abortSignal },
);
return response;
},
});
},
changeCurrentProjectBillingPlanMutationOptions() {
return {
mutationKey: [{ organization, project }, "billing"],
mutationFn: async (data: Rivet.BillingSetPlanRequest) => {
const response = await client.billing.setPlan(project, {
plan: data.plan,
org: organization,
});
return response;
},
};
},
};
};

Expand All @@ -278,7 +315,7 @@ export const createNamespaceContext = ({
...parent,
namespace: engineNamespaceName,
namespaceId: engineNamespaceId,
client: createEngineClient(),
client: createEngineClient(cloudEnv().VITE_APP_CLOUD_ENGINE_URL),
}),
namespaceQueryOptions() {
return parent.currentProjectNamespaceQueryOptions({ namespace });
Expand Down
36 changes: 27 additions & 9 deletions frontend/src/app/data-providers/engine-data-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export type Namespace = {
createdAt: string;
};

export function createClient() {
export function createClient(baseUrl = engineEnv().VITE_APP_API_URL) {
return new RivetClient({
baseUrl: () => engineEnv().VITE_APP_API_URL,
baseUrl: () => baseUrl,
environment: "",
});
}
Expand Down Expand Up @@ -108,6 +108,10 @@ export const createNamespaceContext = ({
statusQueryOptions() {
return queryOptions({
...def.statusQueryOptions(),
queryKey: [
{ namespace, namespaceId },
...def.statusQueryOptions().queryKey,
],
enabled: true,
queryFn: async () => {
return true;
Expand All @@ -118,6 +122,10 @@ export const createNamespaceContext = ({
return infiniteQueryOptions({
...def.regionsQueryOptions(),
enabled: true,
queryKey: [
{ namespace, namespaceId },
...def.regionsQueryOptions().queryKey,
],
queryFn: async () => {
const data = await client.datacenters.list();
return {
Expand All @@ -133,7 +141,10 @@ export const createNamespaceContext = ({
regionQueryOptions(regionId: string | undefined) {
return queryOptions({
...def.regionQueryOptions(regionId),
queryKey: ["region", regionId],
queryKey: [
{ namespace, namespaceId },
...def.regionQueryOptions(regionId).queryKey,
],
queryFn: async ({ client }) => {
const regions = await client.ensureInfiniteQueryData(
this.regionsQueryOptions(),
Expand All @@ -154,7 +165,10 @@ export const createNamespaceContext = ({
actorQueryOptions(actorId) {
return queryOptions({
...def.actorQueryOptions(actorId),
queryKey: [namespace, "actor", actorId],
queryKey: [
{ namespace, namespaceId },
...def.actorQueryOptions(actorId).queryKey,
],
enabled: true,
queryFn: async ({ signal: abortSignal }) => {
const data = await client.actorsGet(
Expand All @@ -170,7 +184,10 @@ export const createNamespaceContext = ({
actorsQueryOptions(opts) {
return infiniteQueryOptions({
...def.actorsQueryOptions(opts),
queryKey: [namespace, "actors", opts],
queryKey: [
{ namespace, namespaceId },
...def.actorsQueryOptions(opts).queryKey,
],
enabled: true,
initialPageParam: undefined,
queryFn: async ({
Expand Down Expand Up @@ -237,7 +254,10 @@ export const createNamespaceContext = ({
buildsQueryOptions() {
return infiniteQueryOptions({
...def.buildsQueryOptions(),
queryKey: [namespace, "builds"],
queryKey: [
{ namespace, namespaceId },
...def.buildsQueryOptions().queryKey,
],
enabled: true,
queryFn: async ({ signal: abortSignal, pageParam }) => {
const data = await client.actorsListNames(
Expand Down Expand Up @@ -402,16 +422,14 @@ export const createNamespaceContext = ({
initialPageParam: undefined as string | undefined,
queryFn: async ({ signal: abortSignal, pageParam }) => {
const response = await client.namespacesRunnerConfigs.list(
namespaceId,
namespace,
{
cursor: pageParam ?? undefined,
limit: RECORDS_PER_PAGE,
},
{ abortSignal },
);

console.log(response);

return response;
},

Expand Down
Loading
Loading