diff --git a/frontend/src/app/actor-builds-list.tsx b/frontend/src/app/actor-builds-list.tsx index 88f32790c4..a97719f086 100644 --- a/frontend/src/app/actor-builds-list.tsx +++ b/frontend/src/app/actor-builds-list.tsx @@ -139,7 +139,7 @@ export function ActorBuildsList() { onClick={() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (navigate as any)({ - to: features.multitenancy + to: features.platform ? "/orgs/$organization/projects/$project/ns/$namespace" : "/ns/$namespace", search: (old: Record) => ({ diff --git a/frontend/src/app/context-switcher.tsx b/frontend/src/app/context-switcher.tsx index 86182fddd7..289390cfa6 100644 --- a/frontend/src/app/context-switcher.tsx +++ b/frontend/src/app/context-switcher.tsx @@ -73,7 +73,7 @@ function ContextSwitcherInner({ }) { const [isOpen, setIsOpen] = useState(false); - if (features.multitenancy) { + if (features.platform) { // biome-ignore lint/correctness/useHookAtTopLevel: guaranteed by build condition usePrefetchInfiniteQuery({ // biome-ignore lint/correctness/useHookAtTopLevel: guaranteed by build condition diff --git a/frontend/src/app/dialogs/create-namespace-frame.tsx b/frontend/src/app/dialogs/create-namespace-frame.tsx index 15afaf18f0..82011c2c08 100644 --- a/frontend/src/app/dialogs/create-namespace-frame.tsx +++ b/frontend/src/app/dialogs/create-namespace-frame.tsx @@ -15,7 +15,7 @@ const useCreateNamespace = ({ project: projectProp }: { project?: string }) => { const navigate = useNavigate(); const params = useParams({ strict: false }); - if (features.multitenancy) { + if (features.platform) { // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant const orgDataProvider = useCloudDataProvider(); const targetProject = projectProp ?? params.project!; diff --git a/frontend/src/app/dialogs/create-project-frame.tsx b/frontend/src/app/dialogs/create-project-frame.tsx index b333f14506..6e614d20de 100644 --- a/frontend/src/app/dialogs/create-project-frame.tsx +++ b/frontend/src/app/dialogs/create-project-frame.tsx @@ -8,7 +8,7 @@ import { authClient } from "@/lib/auth"; import { features } from "@/lib/features"; const useDefaultOrg = () => { - if (features.multitenancy) { + if (features.platform) { // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant const org = authClient.useActiveOrganization(); return org.data?.id; diff --git a/frontend/src/app/dialogs/provide-engine-credentials-frame.tsx b/frontend/src/app/dialogs/provide-engine-credentials-frame.tsx index a6fd539cb6..f2155c8ca1 100644 --- a/frontend/src/app/dialogs/provide-engine-credentials-frame.tsx +++ b/frontend/src/app/dialogs/provide-engine-credentials-frame.tsx @@ -8,10 +8,13 @@ import { ls, toast, } from "@/components"; +import { features } from "@/lib/features"; import { queryClient } from "@/queries/global"; import { TEST_IDS } from "@/utils/test-ids"; import { createClient } from "../data-providers/engine-data-provider"; +const isEnterprise = features.acl && !features.platform; + interface ProvideEngineCredentialsDialogContentProps extends DialogContentProps {} @@ -64,11 +67,15 @@ export default function ProvideEngineCredentialsDialogContent({ }} > - Missing Rivet Engine credentials + + {isEnterprise + ? "Sign in to Rivet Engine" + : "Missing Rivet Engine credentials"} + - It looks like the instance of Rivet Engine that you're - connected to requires additional credentials, please provide - them below. + {isEnterprise + ? "Paste the dashboard token issued to you by your Rivet administrator. See the Rivet Enterprise RBAC documentation for how dashboard tokens are minted." + : "It looks like the instance of Rivet Engine that you're connected to requires additional credentials, please provide them below."} diff --git a/frontend/src/app/dialogs/tokens-frame.tsx b/frontend/src/app/dialogs/tokens-frame.tsx index ecd5ffdb75..7da70d97dc 100644 --- a/frontend/src/app/dialogs/tokens-frame.tsx +++ b/frontend/src/app/dialogs/tokens-frame.tsx @@ -77,7 +77,7 @@ function SecretToken() { const namespace = dataProvider.engineNamespace; - const endpoint = features.multitenancy + const endpoint = features.platform ? regions.find((r) => r.name === selectedDatacenter)?.url || cloudEnv().VITE_APP_API_URL : getConfig().apiUrl; @@ -134,7 +134,7 @@ function PublishableToken() { const namespace = dataProvider.engineNamespace; - const endpoint = features.multitenancy + const endpoint = features.platform ? cloudEnv().VITE_APP_API_URL : getConfig().apiUrl; diff --git a/frontend/src/app/env-variables.stories.tsx b/frontend/src/app/env-variables.stories.tsx new file mode 100644 index 0000000000..f0fb1e0601 --- /dev/null +++ b/frontend/src/app/env-variables.stories.tsx @@ -0,0 +1,144 @@ +import type { Story } from "@ladle/react"; +import "../../.ladle/ladle.css"; +import { DiscreteInput, TooltipProvider } from "@/components"; +import { Label } from "@/components/ui/label"; +import { PublishableTokenNotice } from "./env-variables"; + +function Frame({ children }: { children: React.ReactNode }) { + return ( + +
+
{children}
+
+
+ ); +} + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+
{children}
+
+ ); +} + +const NS = "localhost-ftji-production-ualj"; +const ADMIN_TOKEN = "sk_Km9XaQpL2tRZ3nVxYbCdEfGhJkMnPqRsTuVwXyZaBcDe"; +const PUBLISHABLE_TOKEN = "pk_ivU6QmAj3sKwLp1XnVxYbCdEfGhJkMnPqRsTuVwXyZ"; +const HOST = "engine.example.com"; + +function MockEnvBlock({ publicDsn, secretDsn }: { publicDsn: string; secretDsn: string }) { + return ( +
+ + + + + + +
+ ); +} + +export const PlainOss: Story = () => ( + +
+
+ +
+
+ +); + +export const Cloud: Story = () => ( + +
+
+ +
+
+ +); + +export const EnterpriseAcl: Story = () => ( + +
+
+ + @${HOST}`} + secretDsn={`https://${NS}:${ADMIN_TOKEN}@${HOST}`} + /> +
+
+ +); + +export const NoticeOnly: Story = () => ( + +
+ +
+ +); + +export const Gallery: Story = () => ( + +
+ +
+
+ +
+
+
+ + @${HOST}`} + secretDsn={`https://${NS}:${ADMIN_TOKEN}@${HOST}`} + /> +
+
+ +); diff --git a/frontend/src/app/env-variables.tsx b/frontend/src/app/env-variables.tsx index d210fcba18..ef589d440b 100644 --- a/frontend/src/app/env-variables.tsx +++ b/frontend/src/app/env-variables.tsx @@ -1,3 +1,4 @@ +import { faKey, Icon } from "@rivet-gg/icons"; import { useId } from "react"; import { Button, CopyTrigger, DiscreteInput, getConfig } from "@/components"; import { useEngineCompatDataProvider } from "@/components/actors"; @@ -6,6 +7,8 @@ import { cloudEnv } from "@/lib/env"; import { features } from "@/lib/features"; import { useAdminToken, usePublishableToken } from "@/queries/accessors"; +const needsManualPublishableToken = features.acl && !features.platform; + export function EnvVariables({ id: _id, runnerName, @@ -24,7 +27,10 @@ export function EnvVariables({ const rId = useId(); const id = _id || rId; return ( -
+
+ {showEndpoint && needsManualPublishableToken ? ( + + ) : null}
{ - const globalEndpoint = features.multitenancy + const globalEndpoint = features.platform ? cloudEnv().VITE_APP_API_URL : getConfig().apiUrl; @@ -115,9 +121,9 @@ export const useRivetDsn = ({ const auth = kind === "secret" ? `${namespace}:${adminToken}` - : !features.multitenancy + : !features.acl ? namespace - : `${namespace}:${publishableToken}`; + : `${namespace}:${publishableToken ?? ""}`; const dsn = `https://${auth}@${apiEndpoint .replace("https://", "") @@ -150,6 +156,28 @@ export function RivetPublicEndpointEnv({ ); } +export function PublishableTokenNotice() { + return ( +
+ +
+

Publishable token required

+

+ Replace{" "} + + {""} + {" "} + in RIVET_PUBLIC_ENDPOINT with a token issued + through your Rivet Enterprise RBAC configuration. +

+
+
+ ); +} + export function RivetRunnerEndpointEnv({ prefix, endpoint, diff --git a/frontend/src/app/forms/engine-credentials-form.tsx b/frontend/src/app/forms/engine-credentials-form.tsx index 570340203f..36f3b92572 100644 --- a/frontend/src/app/forms/engine-credentials-form.tsx +++ b/frontend/src/app/forms/engine-credentials-form.tsx @@ -9,6 +9,9 @@ import { FormMessage, Input, } from "@/components"; +import { features } from "@/lib/features"; + +const isEnterprise = features.acl && !features.platform; export const formSchema = z.object({ token: z.string().nonempty("Token is required"), @@ -31,10 +34,16 @@ export const Token = ({ className }: { className?: string }) => { name="token" render={({ field }) => ( - Token + + {isEnterprise ? "Dashboard token" : "Admin token"} + diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 5b06f82134..5f0ea85910 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -57,10 +57,9 @@ import type { HeaderLinkProps } from "@/components/header/header-link"; import { authClient } from "@/lib/auth"; import { features } from "@/lib/features"; import { ensureTrailingSlash } from "@/lib/utils"; -import { TEST_IDS } from "@/utils/test-ids"; import type { RivetActorError } from "@/queries/types"; +import { TEST_IDS } from "@/utils/test-ids"; import { ActorBuildsList } from "./actor-builds-list"; -import { RunnerPoolErrorPopover } from "./runner-pool-error-popover"; import { BillingLimitAlert } from "./billing/billing-limit-alert"; import { BillingPlanBadge } from "./billing/billing-plan-badge"; import { BillingUsageGauge } from "./billing/billing-usage-gauge"; @@ -68,6 +67,7 @@ import { Changelog } from "./changelog"; import { ContextSwitcher } from "./context-switcher"; import { HelpDropdown } from "./help-dropdown"; import { NamespaceSelect } from "./namespace-select"; +import { RunnerPoolErrorPopover } from "./runner-pool-error-popover"; import { UserDropdown } from "./user-dropdown"; interface RootProps { @@ -203,24 +203,25 @@ const Sidebar = ({ >
- {features.multitenancy - ? - : ( - <> - - - - - - )} + {features.platform ? ( + + ) : ( + <> + + + + + + )}
- {features.multitenancy ? ( + {features.platform ? ( <>
- {features.billing && matchRoute({ + {features.billing && + matchRoute({ to: "/orgs/$organization/projects/$project/ns/$namespace", fuzzy: true, pending: false, @@ -244,10 +245,11 @@ const Sidebar = ({
- ) : features.billing && matchRoute({ + ) : features.billing && + matchRoute({ to: "/orgs/$organization/projects/$project", - fuzzy: true, - pending: false, + fuzzy: true, + pending: false, }) ? ( ) : (
- {features.branding ? ( - - - } + {features.branding ? ( + + + } + > + - - What's new? - - - - - ) : null} - - } + What's new? + + + + + ) : null} + + } + > + ({ + ...old, + modal: "feedback", + })} > - ({ - ...old, - modal: "feedback", - })} - > - Feedback - - - - } - endIcon={ - - } + Feedback + + + + } + endIcon={ + + } + > + - - Documentation - - - - } - endIcon={ - - } + Documentation + + + + } + endIcon={ + + } + > + - - Discord - - - - } - endIcon={ - - } + Discord + + + + } + endIcon={ + + } + > + - - GitHub - - + GitHub + +
)}
@@ -667,16 +669,14 @@ function CloudSidebarContentInner() { to: "/orgs/$organization/projects/$project/ns/$namespace", fuzzy: true, }) ? ( -
- - Settings - + + Settings -
+ ) : matchRoute({ to: "/orgs/$organization/projects/$project", fuzzy: true, diff --git a/frontend/src/components/actors/actor-inspector-context.tsx b/frontend/src/components/actors/actor-inspector-context.tsx index 4caaf5c706..b8eee49b05 100644 --- a/frontend/src/components/actors/actor-inspector-context.tsx +++ b/frontend/src/components/actors/actor-inspector-context.tsx @@ -479,19 +479,26 @@ const replayWorkflowFromStepHttp = async ({ entryId, }: { actorId: ActorId; - credentials: { url: string; inspectorToken: string }; + credentials: { url: string; inspectorToken: string; token: string }; entryId?: string; }) => { + const headers: Record = { + Authorization: `Bearer ${credentials.inspectorToken}`, + "Content-Type": "application/json", + "X-Rivet-Target": "actor", + "X-Rivet-Actor": actorId, + }; + if (credentials.token) { + headers["x-rivet-token"] = credentials.token; + } + const response = await fetch( new URL( `${computeActorUrl({ url: credentials.url, actorId })}/inspector/workflow/replay`, ).href, { method: "POST", - headers: { - Authorization: `Bearer ${credentials.inspectorToken}`, - "Content-Type": "application/json", - }, + headers, signal: AbortSignal.timeout(10_000), body: JSON.stringify(entryId ? { entryId } : {}), }, @@ -888,6 +895,7 @@ export const ActorInspectorProvider = ({ credentials: { url: credentials.url, inspectorToken: credentials.inspectorToken, + token: credentials.token, }, entryId, }); diff --git a/frontend/src/components/actors/data-provider.tsx b/frontend/src/components/actors/data-provider.tsx index 8717a647e1..a6434b1f3a 100644 --- a/frontend/src/components/actors/data-provider.tsx +++ b/frontend/src/components/actors/data-provider.tsx @@ -20,7 +20,7 @@ type CloudDataProvider = ReturnType & ReturnType; export const useDataProvider = (): EngineDataProvider | CloudDataProvider => { - if (features.multitenancy) { + if (features.platform) { // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant return useLoaderData({ from: "/_context/orgs/$organization/projects/$project/ns/$namespace", @@ -38,7 +38,7 @@ export const useDataProviderCheck = () => { const matchRoute = useMatchRoute(); return matchRoute({ fuzzy: true, - to: features.multitenancy + to: features.platform ? "/orgs/$organization/projects/$project/ns/$namespace" : "/ns/$namespace", }); @@ -80,7 +80,7 @@ export const useCloudNamespaceDataProvider = () => { }; export const useEngineCompatDataProvider = () => { - if (features.multitenancy) { + if (features.platform) { // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant return useLoaderData({ from: "/_context/orgs/$organization/projects/$project/ns/$namespace", diff --git a/frontend/src/components/actors/guard-connectable-inspector.tsx b/frontend/src/components/actors/guard-connectable-inspector.tsx index 498d406540..a1ac1dee54 100644 --- a/frontend/src/components/actors/guard-connectable-inspector.tsx +++ b/frontend/src/components/actors/guard-connectable-inspector.tsx @@ -23,13 +23,11 @@ import { useMemo, } from "react"; import { match, P } from "ts-pattern"; -import { useLocalStorage } from "usehooks-ts"; import { HelpDropdown } from "@/app/help-dropdown"; import { isRivetApiError } from "@/lib/errors"; import { features } from "@/lib/features"; import { DiscreteCopyButton } from "../copy-area"; -import { getConfig, useConfig } from "../lib/config"; -import { ls } from "../lib/utils"; +import { useConfig } from "../lib/config"; import { ShimmerLine } from "../shimmer-line"; import { Button } from "../ui/button"; import { useFiltersValue } from "./actor-filters-context"; @@ -524,20 +522,18 @@ function useActorRunner({ actorId }: { actorId: ActorId }) { } function useEngineToken() { - if (features.multitenancy) { + if (features.platform) { const { data } = useQuery( useRouteContext({ from: "/_context/orgs/$organization/projects/$project/ns/$namespace", }).dataProvider.publishableTokenQueryOptions(), ); - return data; + return data || ""; } - const [data] = useLocalStorage( - ls.engineCredentials.key(getConfig().apiUrl), - "", - { serializer: JSON.stringify, deserializer: JSON.parse }, + const { data } = useQuery( + useEngineCompatDataProvider().engineAdminTokenQueryOptions(), ); - return data?.token || ""; + return data || ""; } function useEngineUrl() { diff --git a/frontend/src/components/actors/no-providers-alert.tsx b/frontend/src/components/actors/no-providers-alert.tsx index d3426e0d7b..ad141dd72a 100644 --- a/frontend/src/components/actors/no-providers-alert.tsx +++ b/frontend/src/components/actors/no-providers-alert.tsx @@ -29,7 +29,7 @@ export function NoProvidersAlert({
{variant === "default" ? ( <> - {features.multitenancy ? ( + {features.platform ? ( ) : null} - {!features.multitenancy ? ( + {!features.platform ? (