@@ -241,7 +261,7 @@ const Footer = () => {
export { Root, Main, Header, Footer, VisibleInFull, Sidebar };
-const Breadcrumbs = () => {
+const Breadcrumbs = (): ReactNode => {
const matchRoute = useMatchRoute();
const nsMatch = matchRoute({
to: "/ns/$namespace",
@@ -341,14 +361,10 @@ const Subnav = () => {
function HeaderLink({ icon, children, className, ...props }: HeaderLinkProps) {
return (
-
@@ -356,11 +372,26 @@ function HeaderLink({ icon, children, className, ...props }: HeaderLinkProps) {
}
>
{children}
+
+ );
+}
+
+function HeaderButton({ children, className, ...props }: ButtonProps) {
+ return (
+
);
}
-function ConnectionStatus() {
+function ConnectionStatus(): ReactNode {
const { endpoint, ...queries } = useManager();
const { setCredentials } = useInspectorCredentials();
const { isLoading, isError, isSuccess } = useQuery(
@@ -414,4 +445,48 @@ function ConnectionStatus() {
);
}
+
+ return null;
+}
+
+function CloudSidebar(): ReactNode {
+ const match = useMatch({
+ from: "/_layout/orgs/$organization/",
+ shouldThrow: false,
+ });
+
+ const clerk = useClerk();
+ return (
+ <>
+
+
+
+
+
+ Projects
+
+ {
+ clerk.openUserProfile({
+ __experimental_startPath: "/billing",
+ });
+ }}
+ >
+ Billing
+
+ {
+ clerk.openUserProfile();
+ }}
+ >
+ Settings
+
+
+
+ >
+ );
}
diff --git a/frontend/src/app/use-dialog.tsx b/frontend/src/app/use-dialog.tsx
index 10bd1252a9..ecb0e9643d 100644
--- a/frontend/src/app/use-dialog.tsx
+++ b/frontend/src/app/use-dialog.tsx
@@ -8,6 +8,9 @@ export const useDialog = {
CreateNamespace: createDialogHook(
import("@/app/dialogs/create-namespace-dialog"),
),
+ CreateProject: createDialogHook(
+ import("@/app/dialogs/create-project-dialog"),
+ ),
ProvideEngineCredentials: createDialogHook(
import("@/app/dialogs/provide-engine-credentials-dialog"),
),
diff --git a/frontend/src/components/header/header-link.tsx b/frontend/src/components/header/header-link.tsx
index 8dfd50a1bc..60dccc87e1 100644
--- a/frontend/src/components/header/header-link.tsx
+++ b/frontend/src/components/header/header-link.tsx
@@ -29,10 +29,8 @@ export function HeaderLink({
icon ?
: undefined
}
>
-
- {children}
-
-
+ {children}
+
);
}
diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts
new file mode 100644
index 0000000000..b28c4da01b
--- /dev/null
+++ b/frontend/src/lib/auth.ts
@@ -0,0 +1,4 @@
+import { Clerk } from "@clerk/clerk-js";
+import { cloudEnv } from "./env";
+
+export const clerk = new Clerk(cloudEnv().VITE_CLERK_PUBLISHABLE_KEY);
diff --git a/frontend/src/lib/env.ts b/frontend/src/lib/env.ts
new file mode 100644
index 0000000000..89dc93e950
--- /dev/null
+++ b/frontend/src/lib/env.ts
@@ -0,0 +1,9 @@
+import z from "zod";
+
+export const cloudEnvSchema = z.object({
+ VITE_APP_API_URL: z.string().url(),
+ VITE_APP_CLOUD_API_URL: z.string().url(),
+ VITE_CLERK_PUBLISHABLE_KEY: z.string(),
+});
+
+export const cloudEnv = () => cloudEnvSchema.parse(import.meta.env);
diff --git a/frontend/src/queries/manager-cloud.ts b/frontend/src/queries/manager-cloud.ts
new file mode 100644
index 0000000000..ffefdc0fec
--- /dev/null
+++ b/frontend/src/queries/manager-cloud.ts
@@ -0,0 +1,443 @@
+import {
+ type Rivet as CloudRivet,
+ RivetClient as CloudRivetClient,
+} from "@rivet-gg/cloud";
+import { ActorFeature } from "@rivetkit/core/inspector";
+import { type Rivet, RivetClient } from "@rivetkit/engine-api-full";
+import {
+ infiniteQueryOptions,
+ queryOptions,
+ skipToken,
+} from "@tanstack/react-query";
+import z from "zod";
+import { getConfig } from "@/components";
+import type {
+ Actor,
+ ActorId,
+ CrashPolicy,
+ ManagerContext,
+} from "@/components/actors";
+import {
+ ACTORS_PER_PAGE,
+ ActorQueryOptionsSchema,
+ createDefaultManagerContext,
+} from "@/components/actors/manager-context";
+import { clerk } from "@/lib/auth";
+import { cloudEnv } from "@/lib/env";
+
+const client = new RivetClient({
+ baseUrl: () => getConfig().apiUrl,
+ environment: "",
+});
+
+const cloudClient = new CloudRivetClient({
+ baseUrl: () => cloudEnv().VITE_APP_CLOUD_API_URL,
+ environment: "",
+ token: async () => {
+ console.log(await clerk.session?.getToken());
+ return (await clerk.session?.getToken()) || "";
+ },
+});
+
+export { client as managerClient };
+
+export const createCloudManagerContext = ({
+ namespace,
+}: {
+ namespace: string;
+}) => {
+ const def = createDefaultManagerContext();
+ return {
+ ...def,
+ features: {
+ canCreateActors: true,
+ canDeleteActors: true,
+ },
+ managerStatusQueryOptions() {
+ return queryOptions({
+ ...def.managerStatusQueryOptions(),
+ enabled: true,
+ queryFn: async () => {
+ return true;
+ },
+ });
+ },
+ regionsQueryOptions() {
+ return infiniteQueryOptions({
+ ...def.regionsQueryOptions(),
+ enabled: true,
+ queryFn: async () => {
+ const data = await client.datacenters.list();
+ return {
+ regions: data.datacenters.map((dc) => ({
+ id: dc.name,
+ name: dc.name,
+ })),
+ pagination: data.pagination,
+ };
+ },
+ });
+ },
+ regionQueryOptions(regionId: string) {
+ return queryOptions({
+ ...def.regionQueryOptions(regionId),
+ queryKey: ["region", regionId],
+ queryFn: async ({ client }) => {
+ const regions = await client.ensureInfiniteQueryData(
+ this.regionsQueryOptions(),
+ );
+
+ for (const page of regions.pages) {
+ for (const region of page.regions) {
+ if (region.id === regionId) {
+ return region;
+ }
+ }
+ }
+
+ throw new Error(`Region not found: ${regionId}`);
+ },
+ });
+ },
+ actorQueryOptions(actorId) {
+ return queryOptions({
+ ...def.actorQueryOptions(actorId),
+ queryKey: [namespace, "actor", actorId],
+ enabled: true,
+ queryFn: async ({ signal: abortSignal }) => {
+ const data = await client.actorsGet(
+ actorId,
+ { namespace },
+ { abortSignal },
+ );
+
+ return transformActor(data.actor);
+ },
+ });
+ },
+ actorsQueryOptions(opts) {
+ return infiniteQueryOptions({
+ ...def.actorsQueryOptions(opts),
+ queryKey: [namespace, "actors", opts],
+ enabled: true,
+ initialPageParam: undefined,
+ queryFn: async ({
+ signal: abortSignal,
+ pageParam,
+ queryKey: [, , _opts],
+ }) => {
+ const { success, data: opts } =
+ ActorQueryOptionsSchema.safeParse(_opts || {});
+
+ if (
+ (opts?.n?.length === 0 || !opts?.n) &&
+ (opts?.filters?.id?.value?.length === 0 ||
+ !opts?.filters?.id?.value ||
+ opts?.filters.key?.value?.length === 0 ||
+ !opts?.filters.key?.value)
+ ) {
+ // If there are no names specified, we can return an empty result
+ return {
+ actors: [],
+ pagination: {
+ cursor: undefined,
+ },
+ };
+ }
+
+ const data = await client.actorsList(
+ {
+ namespace,
+ cursor: pageParam ?? undefined,
+ actorIds: opts?.filters?.id?.value?.join(","),
+ key: opts?.filters?.key?.value?.join(","),
+ includeDestroyed:
+ success &&
+ (opts?.filters?.showDestroyed?.value.includes(
+ "true",
+ ) ||
+ opts?.filters?.showDestroyed?.value.includes(
+ "1",
+ )),
+ limit: ACTORS_PER_PAGE,
+ name: opts?.filters?.id?.value
+ ? undefined
+ : opts?.n?.join(","),
+ },
+ { abortSignal },
+ );
+
+ return {
+ ...data,
+ pagination: {
+ cursor: data.pagination.cursor || null,
+ },
+ actors: data.actors.map((actor) =>
+ transformActor(actor),
+ ),
+ };
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.actors.length < ACTORS_PER_PAGE) {
+ return undefined;
+ }
+ return lastPage.pagination.cursor;
+ },
+ });
+ },
+ buildsQueryOptions() {
+ return infiniteQueryOptions({
+ ...def.buildsQueryOptions(),
+ queryKey: [namespace, "builds"],
+ enabled: true,
+ queryFn: async ({ signal: abortSignal, pageParam }) => {
+ const data = await client.actorsListNames(
+ {
+ namespace,
+ cursor: pageParam ?? undefined,
+ limit: ACTORS_PER_PAGE,
+ },
+ { abortSignal },
+ );
+
+ return {
+ pagination: data.pagination,
+ builds: Object.keys(data.names)
+ .sort()
+ .map((build) => ({
+ id: build,
+ name: build,
+ })),
+ };
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.builds.length < ACTORS_PER_PAGE) {
+ return undefined;
+ }
+ return lastPage.pagination.cursor;
+ },
+ });
+ },
+ createActorMutationOptions() {
+ return {
+ ...def.createActorMutationOptions(),
+ mutationKey: [namespace, "actors"],
+ mutationFn: async (data) => {
+ const response = await client.actorsCreate({
+ namespace,
+ name: data.name,
+ key: data.key,
+ crashPolicy: data.crashPolicy,
+ runnerNameSelector: data.runnerNameSelector,
+ input: JSON.stringify(data.input),
+ });
+
+ return response.actor.actorId;
+ },
+ onSuccess: () => {},
+ };
+ },
+ actorDestroyMutationOptions(actorId) {
+ return {
+ ...def.actorDestroyMutationOptions(actorId),
+ mutationFn: async () => {
+ await client.actorsDelete(actorId);
+ },
+ };
+ },
+ } satisfies ManagerContext;
+};
+
+export const NamespaceNameId = z.string().brand();
+export type NamespaceNameId = z.infer
;
+
+export const projectsQueryOptions = ({ orgId }: { orgId: string }) => {
+ return infiniteQueryOptions({
+ queryKey: [orgId, "projects"],
+ initialPageParam: undefined as string | undefined,
+ queryFn: async ({ signal: abortSignal, pageParam }) => {
+ const data = await cloudClient.projects.list(
+ {
+ cursor: pageParam ?? undefined,
+ limit: ACTORS_PER_PAGE,
+ },
+ {
+ abortSignal,
+ },
+ );
+ return data;
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.projects.length < ACTORS_PER_PAGE) {
+ return undefined;
+ }
+ return lastPage.pagination.cursor;
+ },
+ select: (data) => data.pages.flatMap((page) => page.projects),
+ });
+};
+
+export const runnersQueryOptions = (opts: { namespace: NamespaceNameId }) => {
+ return infiniteQueryOptions({
+ queryKey: [opts.namespace, "runners"],
+ initialPageParam: undefined as string | undefined,
+ queryFn: async ({ pageParam, signal: abortSignal }) => {
+ const data = await client.runners.list(
+ {
+ namespace: opts.namespace,
+ cursor: pageParam ?? undefined,
+ limit: ACTORS_PER_PAGE,
+ },
+ { abortSignal },
+ );
+ return data;
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.runners.length < ACTORS_PER_PAGE) {
+ return undefined;
+ }
+ return lastPage.pagination.cursor;
+ },
+ select: (data) => data.pages.flatMap((page) => page.runners),
+ });
+};
+
+export const runnerQueryOptions = (opts: {
+ namespace: NamespaceNameId;
+ runnerId: string;
+}) => {
+ return queryOptions({
+ queryKey: [opts.namespace, "runner", opts.runnerId],
+ enabled: !!opts.runnerId,
+ queryFn: async ({ signal: abortSignal }) => {
+ const data = await client.runners.get(
+ opts.runnerId,
+ { namespace: opts.namespace },
+ {
+ abortSignal,
+ },
+ );
+ return data.runner;
+ },
+ });
+};
+
+export const runnerNamesQueryOptions = (opts: {
+ namespace: NamespaceNameId;
+}) => {
+ return infiniteQueryOptions({
+ queryKey: [opts.namespace, "runner", "names"],
+ initialPageParam: undefined as string | undefined,
+ queryFn: async ({ signal: abortSignal, pageParam }) => {
+ const data = await client.runners.listNames(
+ {
+ namespace: opts.namespace,
+ cursor: pageParam ?? undefined,
+ limit: ACTORS_PER_PAGE,
+ },
+ {
+ abortSignal,
+ },
+ );
+ return data;
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.names.length < ACTORS_PER_PAGE) {
+ return undefined;
+ }
+ return lastPage.pagination.cursor;
+ },
+ select: (data) => data.pages.flatMap((page) => page.names),
+ });
+};
+
+export const namespacesQueryOptions = () => {
+ return infiniteQueryOptions({
+ queryKey: ["namespaces"],
+ initialPageParam: undefined as string | undefined,
+ queryFn: async ({ pageParam, signal: abortSignal }) => {
+ const data = await client.namespaces.list(
+ {
+ limit: ACTORS_PER_PAGE,
+ cursor: pageParam ?? undefined,
+ },
+ { abortSignal },
+ );
+ return data;
+ },
+ getNextPageParam: (lastPage) => {
+ if (lastPage.namespaces.length < ACTORS_PER_PAGE) {
+ return undefined;
+ }
+ return lastPage.pagination.cursor;
+ },
+ select: (data) => data.pages.flatMap((page) => page.namespaces),
+ });
+};
+
+export const namespaceQueryOptions = (
+ namespace: NamespaceNameId | undefined,
+) => {
+ return queryOptions({
+ queryKey: ["namespace", namespace],
+ enabled: !!namespace,
+ queryFn: namespace
+ ? async ({ signal: abortSignal }) => {
+ const data = await client.namespaces.get(namespace, {
+ abortSignal,
+ });
+ return data.namespace;
+ }
+ : skipToken,
+ });
+};
+
+function transformActor(a: Rivet.Actor): Actor {
+ return {
+ id: a.actorId as ActorId,
+ name: a.name,
+ key: a.key ? a.key : undefined,
+ connectableAt: a.connectableTs
+ ? new Date(a.connectableTs).toISOString()
+ : undefined,
+ region: a.datacenter,
+ createdAt: new Date(a.createTs).toISOString(),
+ startedAt: a.startTs ? new Date(a.startTs).toISOString() : undefined,
+ destroyedAt: a.destroyTs
+ ? new Date(a.destroyTs).toISOString()
+ : undefined,
+ sleepingAt: a.sleepTs ? new Date(a.sleepTs).toISOString() : undefined,
+ pendingAllocationAt: a.pendingAllocationTs
+ ? new Date(a.pendingAllocationTs).toISOString()
+ : undefined,
+ crashPolicy: a.crashPolicy as CrashPolicy,
+ runner: a.runnerNameSelector,
+ features: [
+ ActorFeature.Config,
+ ActorFeature.Connections,
+ ActorFeature.State,
+ ActorFeature.Console,
+ ActorFeature.Database,
+ ActorFeature.EventsMonitoring,
+ ],
+ };
+}
+
+export function createProjectMutationOptions({
+ onSuccess,
+}: {
+ onSuccess?: (data: CloudRivet.Project) => void;
+} = {}) {
+ return {
+ mutationKey: ["projects"],
+ mutationFn: async (data: { displayName: string; nameId: string }) => {
+ const response = await cloudClient.projects.create({
+ displayName: data.displayName,
+ name: data.nameId,
+ });
+
+ return response;
+ },
+ onSuccess,
+ };
+}
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 3f8bd2201b..56afa0cb05 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -13,8 +13,10 @@ import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
import { Route as LayoutNamespacesRouteImport } from './routes/_layout/namespaces'
import { Route as LayoutNsNamespaceRouteImport } from './routes/_layout/ns.$namespace'
+import { Route as LayoutOrgsOrganizationIndexRouteImport } from './routes/_layout/orgs.$organization/index'
import { Route as LayoutNsNamespaceIndexRouteImport } from './routes/_layout/ns.$namespace/index'
import { Route as LayoutNsNamespaceRunnersRouteImport } from './routes/_layout/ns.$namespace/runners'
+import { Route as LayoutOrgsOrganizationProjectsProjectIndexRouteImport } from './routes/_layout/orgs.$organization/projects.$project/index'
const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
@@ -35,6 +37,12 @@ const LayoutNsNamespaceRoute = LayoutNsNamespaceRouteImport.update({
path: '/ns/$namespace',
getParentRoute: () => LayoutRoute,
} as any)
+const LayoutOrgsOrganizationIndexRoute =
+ LayoutOrgsOrganizationIndexRouteImport.update({
+ id: '/orgs/$organization/',
+ path: '/orgs/$organization/',
+ getParentRoute: () => LayoutRoute,
+ } as any)
const LayoutNsNamespaceIndexRoute = LayoutNsNamespaceIndexRouteImport.update({
id: '/',
path: '/',
@@ -46,6 +54,12 @@ const LayoutNsNamespaceRunnersRoute =
path: '/runners',
getParentRoute: () => LayoutNsNamespaceRoute,
} as any)
+const LayoutOrgsOrganizationProjectsProjectIndexRoute =
+ LayoutOrgsOrganizationProjectsProjectIndexRouteImport.update({
+ id: '/orgs/$organization/projects/$project/',
+ path: '/orgs/$organization/projects/$project/',
+ getParentRoute: () => LayoutRoute,
+ } as any)
export interface FileRoutesByFullPath {
'/namespaces': typeof LayoutNamespacesRoute
@@ -53,12 +67,16 @@ export interface FileRoutesByFullPath {
'/ns/$namespace': typeof LayoutNsNamespaceRouteWithChildren
'/ns/$namespace/runners': typeof LayoutNsNamespaceRunnersRoute
'/ns/$namespace/': typeof LayoutNsNamespaceIndexRoute
+ '/orgs/$organization': typeof LayoutOrgsOrganizationIndexRoute
+ '/orgs/$organization/projects/$project': typeof LayoutOrgsOrganizationProjectsProjectIndexRoute
}
export interface FileRoutesByTo {
'/namespaces': typeof LayoutNamespacesRoute
'/': typeof LayoutIndexRoute
'/ns/$namespace/runners': typeof LayoutNsNamespaceRunnersRoute
'/ns/$namespace': typeof LayoutNsNamespaceIndexRoute
+ '/orgs/$organization': typeof LayoutOrgsOrganizationIndexRoute
+ '/orgs/$organization/projects/$project': typeof LayoutOrgsOrganizationProjectsProjectIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -68,6 +86,8 @@ export interface FileRoutesById {
'/_layout/ns/$namespace': typeof LayoutNsNamespaceRouteWithChildren
'/_layout/ns/$namespace/runners': typeof LayoutNsNamespaceRunnersRoute
'/_layout/ns/$namespace/': typeof LayoutNsNamespaceIndexRoute
+ '/_layout/orgs/$organization/': typeof LayoutOrgsOrganizationIndexRoute
+ '/_layout/orgs/$organization/projects/$project/': typeof LayoutOrgsOrganizationProjectsProjectIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -77,8 +97,16 @@ export interface FileRouteTypes {
| '/ns/$namespace'
| '/ns/$namespace/runners'
| '/ns/$namespace/'
+ | '/orgs/$organization'
+ | '/orgs/$organization/projects/$project'
fileRoutesByTo: FileRoutesByTo
- to: '/namespaces' | '/' | '/ns/$namespace/runners' | '/ns/$namespace'
+ to:
+ | '/namespaces'
+ | '/'
+ | '/ns/$namespace/runners'
+ | '/ns/$namespace'
+ | '/orgs/$organization'
+ | '/orgs/$organization/projects/$project'
id:
| '__root__'
| '/_layout'
@@ -87,6 +115,8 @@ export interface FileRouteTypes {
| '/_layout/ns/$namespace'
| '/_layout/ns/$namespace/runners'
| '/_layout/ns/$namespace/'
+ | '/_layout/orgs/$organization/'
+ | '/_layout/orgs/$organization/projects/$project/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -123,6 +153,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutNsNamespaceRouteImport
parentRoute: typeof LayoutRoute
}
+ '/_layout/orgs/$organization/': {
+ id: '/_layout/orgs/$organization/'
+ path: '/orgs/$organization'
+ fullPath: '/orgs/$organization'
+ preLoaderRoute: typeof LayoutOrgsOrganizationIndexRouteImport
+ parentRoute: typeof LayoutRoute
+ }
'/_layout/ns/$namespace/': {
id: '/_layout/ns/$namespace/'
path: '/'
@@ -137,6 +174,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutNsNamespaceRunnersRouteImport
parentRoute: typeof LayoutNsNamespaceRoute
}
+ '/_layout/orgs/$organization/projects/$project/': {
+ id: '/_layout/orgs/$organization/projects/$project/'
+ path: '/orgs/$organization/projects/$project'
+ fullPath: '/orgs/$organization/projects/$project'
+ preLoaderRoute: typeof LayoutOrgsOrganizationProjectsProjectIndexRouteImport
+ parentRoute: typeof LayoutRoute
+ }
}
}
@@ -157,12 +201,17 @@ interface LayoutRouteChildren {
LayoutNamespacesRoute: typeof LayoutNamespacesRoute
LayoutIndexRoute: typeof LayoutIndexRoute
LayoutNsNamespaceRoute: typeof LayoutNsNamespaceRouteWithChildren
+ LayoutOrgsOrganizationIndexRoute: typeof LayoutOrgsOrganizationIndexRoute
+ LayoutOrgsOrganizationProjectsProjectIndexRoute: typeof LayoutOrgsOrganizationProjectsProjectIndexRoute
}
const LayoutRouteChildren: LayoutRouteChildren = {
LayoutNamespacesRoute: LayoutNamespacesRoute,
LayoutIndexRoute: LayoutIndexRoute,
LayoutNsNamespaceRoute: LayoutNsNamespaceRouteWithChildren,
+ LayoutOrgsOrganizationIndexRoute: LayoutOrgsOrganizationIndexRoute,
+ LayoutOrgsOrganizationProjectsProjectIndexRoute:
+ LayoutOrgsOrganizationProjectsProjectIndexRoute,
}
const LayoutRouteWithChildren =
diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx
index ff40ffae0f..5f05b309cb 100644
--- a/frontend/src/routes/__root.tsx
+++ b/frontend/src/routes/__root.tsx
@@ -1,6 +1,10 @@
+import { ClerkProvider } from "@clerk/clerk-react";
+import { dark } from "@clerk/themes";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
+import { match } from "ts-pattern";
import { FullscreenLoading } from "@/components";
+import { clerk } from "@/lib/auth";
function RootRoute() {
return (
@@ -13,8 +17,28 @@ function RootRoute() {
);
}
+function CloudRootRoute() {
+ const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
+
+ if (!PUBLISHABLE_KEY) {
+ throw new Error("Add your Clerk Publishable Key to the .env file");
+ }
+
+ return (
+
+
+
+ );
+}
+
export const Route = createRootRouteWithContext()({
- component: RootRoute,
+ component: match(__APP_TYPE__)
+ .with("cloud", () => CloudRootRoute)
+ .otherwise(() => RootRoute),
pendingComponent: FullscreenLoading,
wrapInSuspense: true,
});
diff --git a/frontend/src/routes/_layout.tsx b/frontend/src/routes/_layout.tsx
index aad64c9f85..70985195aa 100644
--- a/frontend/src/routes/_layout.tsx
+++ b/frontend/src/routes/_layout.tsx
@@ -1,6 +1,19 @@
+import {
+ SignedIn,
+ SignedOut,
+ SignInButton,
+ useOrganization,
+} from "@clerk/clerk-react";
import { faNodeJs, faReact, Icon } from "@rivet-gg/icons";
-import { createFileRoute, Outlet, useMatch } from "@tanstack/react-router";
+import {
+ createFileRoute,
+ Navigate,
+ Outlet,
+ useMatch,
+ useNavigate,
+} from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
+import { C } from "node_modules/@clerk/clerk-react/dist/useAuth-BVxIa9U7.mjs";
import { usePostHog } from "posthog-js/react";
import {
type ComponentProps,
@@ -12,6 +25,7 @@ import {
useRef,
useState,
} from "react";
+import { match } from "ts-pattern";
import z from "zod";
import {
type InspectorCredentials,
@@ -61,7 +75,9 @@ const searchSchema = z
export const Route = createFileRoute("/_layout")({
validateSearch: zodValidator(searchSchema),
- component: RouteComponent,
+ component: match(__APP_TYPE__)
+ .with("cloud", () => CloudRouteComponent)
+ .otherwise(() => RouteComponent),
});
function RouteComponent() {
@@ -386,3 +402,60 @@ function Connect({
);
}
+
+function CloudRouteComponent() {
+ return (
+ <>
+