diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx
index 544f53bfa8..65425fc339 100644
--- a/frontend/src/app.tsx
+++ b/frontend/src/app.tsx
@@ -19,6 +19,7 @@ import {
Toaster,
TooltipProvider,
} from "@/components";
+import { NotFoundCard } from "./app/not-found-card";
import { RouteLayout } from "./app/route-layout";
import { RootLayout } from "./components/layout";
import { clerk } from "./lib/auth";
@@ -60,27 +61,7 @@ export const router = createRouter({
},
defaultNotFoundComponent: () => (
-
-
-
-
-
-
- 404
-
-
- The page was not found
-
-
-
-
-
-
-
-
-
+
),
});
diff --git a/frontend/src/app/data-providers/cloud-data-provider.tsx b/frontend/src/app/data-providers/cloud-data-provider.tsx
index bda157e16b..2068702ffd 100644
--- a/frontend/src/app/data-providers/cloud-data-provider.tsx
+++ b/frontend/src/app/data-providers/cloud-data-provider.tsx
@@ -381,5 +381,29 @@ export const createNamespaceContext = ({
},
});
},
+ publishableTokenQueryOptions() {
+ return queryOptions({
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ gcTime: 5 * 60 * 1000, // 5 minutes
+ queryKey: [
+ {
+ namespace,
+ project: parent.project,
+ organization: parent.organization,
+ },
+ "tokens",
+ "publishable",
+ ],
+ queryFn: async () => {
+ const f = parent.client.namespaces.createPublishableToken(
+ parent.project,
+ namespace,
+ { org: parent.organization },
+ );
+ const t = await f;
+ return t.token;
+ },
+ });
+ },
};
};
diff --git a/frontend/src/app/data-providers/default-data-provider.tsx b/frontend/src/app/data-providers/default-data-provider.tsx
index 893de148cf..678ae98dab 100644
--- a/frontend/src/app/data-providers/default-data-provider.tsx
+++ b/frontend/src/app/data-providers/default-data-provider.tsx
@@ -209,6 +209,7 @@ const defaultContext = {
: null,
sleepingAt: data.sleepingAt ? new Date(data.sleepingAt) : null,
region: data.region,
+ runner: data.runner,
crashPolicy: data.crashPolicy,
}),
});
diff --git a/frontend/src/app/dialogs/tokens-frame.tsx b/frontend/src/app/dialogs/tokens-frame.tsx
new file mode 100644
index 0000000000..ac432aa2cf
--- /dev/null
+++ b/frontend/src/app/dialogs/tokens-frame.tsx
@@ -0,0 +1,103 @@
+import { faQuestionCircle, faRailway, Icon } from "@rivet-gg/icons";
+import { useQuery } from "@tanstack/react-query";
+import { useRouteContext } from "@tanstack/react-router";
+import * as ConnectRailwayForm from "@/app/forms/connect-railway-form";
+import { HelpDropdown } from "@/app/help-dropdown";
+import {
+ Button,
+ type DialogContentProps,
+ DiscreteInput,
+ Frame,
+ Label,
+ Skeleton,
+} from "@/components";
+import {
+ useCloudDataProvider,
+ useEngineCompatDataProvider,
+} from "@/components/actors";
+import { defineStepper } from "@/components/ui/stepper";
+import { cloudEnv, engineEnv } from "@/lib/env";
+
+interface TokensFrameContentProps extends DialogContentProps {}
+
+export default function TokensFrameContent({
+ onClose,
+}: TokensFrameContentProps) {
+ return (
+ <>
+
+
+ Namespace Tokens
+
+
+
+
+
+ These tokens are used to authenticate requests to the Rivet
+ Engine API. Keep them secret!
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function SecretToken() {
+ const dataProvider = useRouteContext({
+ from: "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace",
+ select: (c) => c.dataProvider,
+ });
+ const { data, isLoading } = useQuery(
+ dataProvider.engineAdminTokenQueryOptions(),
+ );
+ return (
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ Only use in secure server environments. Grants full access to
+ your namespace. Used to connect your Runners to your namespace.
+
+
+ );
+}
+
+function PublishableToken() {
+ const dataProvider = useRouteContext({
+ from: "/_context/_cloud/orgs/$organization/projects/$project/ns/$namespace",
+ select: (c) => c.dataProvider,
+ });
+ const { data, isLoading } = useQuery(
+ dataProvider.publishableTokenQueryOptions(),
+ );
+ return (
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ Safe to use in public contexts like client-side code. Allows
+ your frontend to interact with Rivet services.
+
+
+ );
+}
diff --git a/frontend/src/app/not-found-card.tsx b/frontend/src/app/not-found-card.tsx
new file mode 100644
index 0000000000..62e3049e6e
--- /dev/null
+++ b/frontend/src/app/not-found-card.tsx
@@ -0,0 +1,35 @@
+import { Link } from "@tanstack/react-router";
+import {
+ Button,
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components";
+
+export function NotFoundCard() {
+ return (
+
+
+
+
+
+
+ 404
+
+
+ The page was not found
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/use-dialog.tsx b/frontend/src/app/use-dialog.tsx
index c9ba831fb4..9e6a720ccd 100644
--- a/frontend/src/app/use-dialog.tsx
+++ b/frontend/src/app/use-dialog.tsx
@@ -18,4 +18,5 @@ export const useDialog = {
ProvideEngineCredentials: createDialogHook(
() => import("@/app/dialogs/provide-engine-credentials-frame"),
),
+ Tokens: createDialogHook(() => import("@/app/dialogs/tokens-frame")),
};
diff --git a/frontend/src/app/user-dropdown.tsx b/frontend/src/app/user-dropdown.tsx
index bfa4a03f84..8ead4b57c8 100644
--- a/frontend/src/app/user-dropdown.tsx
+++ b/frontend/src/app/user-dropdown.tsx
@@ -59,6 +59,16 @@ export function UserDropdown() {
>
Profile
+ {
+ navigate({
+ to: ".",
+ search: { ...params, modal: "tokens" },
+ });
+ }}
+ >
+ Tokens
+
{clerk.organization ? (
{
diff --git a/frontend/src/components/actors/actor-general.tsx b/frontend/src/components/actors/actor-general.tsx
index 1779c623b7..cee0203071 100644
--- a/frontend/src/components/actors/actor-general.tsx
+++ b/frontend/src/components/actors/actor-general.tsx
@@ -21,6 +21,7 @@ export function ActorGeneral({ actorId }: ActorGeneralProps) {
pendingAllocationAt,
sleepingAt,
crashPolicy,
+ runner,
} = {},
} = useQuery(useDataProvider().actorGeneralQueryOptions(actorId));
@@ -61,6 +62,8 @@ export function ActorGeneral({ actorId }: ActorGeneralProps) {
/>
+ Runner
+ {runner}
Crash Policy
{crashPolicy}
Created
diff --git a/frontend/src/routes/_context/_cloud.tsx b/frontend/src/routes/_context/_cloud.tsx
index 95e6bb173b..0216536a8b 100644
--- a/frontend/src/routes/_context/_cloud.tsx
+++ b/frontend/src/routes/_context/_cloud.tsx
@@ -39,6 +39,7 @@ function CloudModals() {
const CreateNamespaceDialog = useDialog.CreateNamespace.Dialog;
const ConnectVercelDialog = useDialog.ConnectVercel.Dialog;
const ConnectRailwayDialog = useDialog.ConnectRailway.Dialog;
+ const TokensDialog = useDialog.Tokens.Dialog;
return (
<>
@@ -116,6 +117,23 @@ function CloudModals() {
},
}}
/>
+ {
+ if (!value) {
+ navigate({
+ to: ".",
+ search: (old) => ({
+ ...old,
+ modal: undefined,
+ }),
+ });
+ }
+ },
+ }}
+ />
>
);
}
diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project.tsx
index 4868883e80..8931705fc0 100644
--- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project.tsx
+++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project.tsx
@@ -1,6 +1,7 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { match } from "ts-pattern";
import { createProjectContext } from "@/app/data-providers/cloud-data-provider";
+import { NotFoundCard } from "@/app/not-found-card";
import { RouteError } from "@/app/route-error";
import { useDialog } from "@/app/use-dialog";
diff --git a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx
index af58b8a0c6..eec565c29e 100644
--- a/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx
+++ b/frontend/src/routes/_context/_cloud/orgs.$organization/projects.$project/ns.$namespace.tsx
@@ -1,5 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { createNamespaceContext } from "@/app/data-providers/cloud-data-provider";
+import { NotFoundCard } from "@/app/not-found-card";
import { RouteLayout } from "@/app/route-layout";
export const Route = createFileRoute(
@@ -28,6 +29,7 @@ export const Route = createFileRoute(
},
};
},
+ notFoundComponent: () => ,
});
function RouteComponent() {
diff --git a/frontend/src/routes/_context/_engine/ns.$namespace.tsx b/frontend/src/routes/_context/_engine/ns.$namespace.tsx
index c272104963..9b9a2d7e54 100644
--- a/frontend/src/routes/_context/_engine/ns.$namespace.tsx
+++ b/frontend/src/routes/_context/_engine/ns.$namespace.tsx
@@ -1,6 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import { match } from "ts-pattern";
import { createNamespaceContext } from "@/app/data-providers/engine-data-provider";
+import { NotFoundCard } from "@/app/not-found-card";
import { RouteLayout } from "@/app/route-layout";
export const Route = createFileRoute("/_context/_engine/ns/$namespace")({
@@ -20,6 +21,7 @@ export const Route = createFileRoute("/_context/_engine/ns/$namespace")({
});
},
component: RouteComponent,
+ notFoundComponent: () => ,
});
function RouteComponent() {