Skip to content
Merged
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
42 changes: 42 additions & 0 deletions apps/cloud/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

import { Route as rootRouteImport } from './routes/__root'
import { Route as ToolsRouteImport } from './routes/tools'
import { Route as SetupMcpRouteImport } from './routes/setup-mcp'
import { Route as SecretsRouteImport } from './routes/secrets'
import { Route as PoliciesRouteImport } from './routes/policies'
import { Route as OrgRouteImport } from './routes/org'
import { Route as CreateOrgRouteImport } from './routes/create-org'
import { Route as ConnectionsRouteImport } from './routes/connections'
import { Route as BillingRouteImport } from './routes/billing'
import { Route as ApiKeysRouteImport } from './routes/api-keys'
Expand All @@ -26,6 +28,11 @@ const ToolsRoute = ToolsRouteImport.update({
path: '/tools',
getParentRoute: () => rootRouteImport,
} as any)
const SetupMcpRoute = SetupMcpRouteImport.update({
id: '/setup-mcp',
path: '/setup-mcp',
getParentRoute: () => rootRouteImport,
} as any)
const SecretsRoute = SecretsRouteImport.update({
id: '/secrets',
path: '/secrets',
Expand All @@ -41,6 +48,11 @@ const OrgRoute = OrgRouteImport.update({
path: '/org',
getParentRoute: () => rootRouteImport,
} as any)
const CreateOrgRoute = CreateOrgRouteImport.update({
id: '/create-org',
path: '/create-org',
getParentRoute: () => rootRouteImport,
} as any)
const ConnectionsRoute = ConnectionsRouteImport.update({
id: '/connections',
path: '/connections',
Expand Down Expand Up @@ -82,9 +94,11 @@ export interface FileRoutesByFullPath {
'/api-keys': typeof ApiKeysRoute
'/billing': typeof BillingRoute
'/connections': typeof ConnectionsRoute
'/create-org': typeof CreateOrgRoute
'/org': typeof OrgRoute
'/policies': typeof PoliciesRoute
'/secrets': typeof SecretsRoute
'/setup-mcp': typeof SetupMcpRoute
'/tools': typeof ToolsRoute
'/billing/plans': typeof BillingPlansRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
Expand All @@ -95,9 +109,11 @@ export interface FileRoutesByTo {
'/api-keys': typeof ApiKeysRoute
'/billing': typeof BillingRoute
'/connections': typeof ConnectionsRoute
'/create-org': typeof CreateOrgRoute
'/org': typeof OrgRoute
'/policies': typeof PoliciesRoute
'/secrets': typeof SecretsRoute
'/setup-mcp': typeof SetupMcpRoute
'/tools': typeof ToolsRoute
'/billing/plans': typeof BillingPlansRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
Expand All @@ -109,9 +125,11 @@ export interface FileRoutesById {
'/api-keys': typeof ApiKeysRoute
'/billing': typeof BillingRoute
'/connections': typeof ConnectionsRoute
'/create-org': typeof CreateOrgRoute
'/org': typeof OrgRoute
'/policies': typeof PoliciesRoute
'/secrets': typeof SecretsRoute
'/setup-mcp': typeof SetupMcpRoute
'/tools': typeof ToolsRoute
'/billing_/plans': typeof BillingPlansRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
Expand All @@ -124,9 +142,11 @@ export interface FileRouteTypes {
| '/api-keys'
| '/billing'
| '/connections'
| '/create-org'
| '/org'
| '/policies'
| '/secrets'
| '/setup-mcp'
| '/tools'
| '/billing/plans'
| '/sources/$namespace'
Expand All @@ -137,9 +157,11 @@ export interface FileRouteTypes {
| '/api-keys'
| '/billing'
| '/connections'
| '/create-org'
| '/org'
| '/policies'
| '/secrets'
| '/setup-mcp'
| '/tools'
| '/billing/plans'
| '/sources/$namespace'
Expand All @@ -150,9 +172,11 @@ export interface FileRouteTypes {
| '/api-keys'
| '/billing'
| '/connections'
| '/create-org'
| '/org'
| '/policies'
| '/secrets'
| '/setup-mcp'
| '/tools'
| '/billing_/plans'
| '/sources/$namespace'
Expand All @@ -164,9 +188,11 @@ export interface RootRouteChildren {
ApiKeysRoute: typeof ApiKeysRoute
BillingRoute: typeof BillingRoute
ConnectionsRoute: typeof ConnectionsRoute
CreateOrgRoute: typeof CreateOrgRoute
OrgRoute: typeof OrgRoute
PoliciesRoute: typeof PoliciesRoute
SecretsRoute: typeof SecretsRoute
SetupMcpRoute: typeof SetupMcpRoute
ToolsRoute: typeof ToolsRoute
BillingPlansRoute: typeof BillingPlansRoute
SourcesNamespaceRoute: typeof SourcesNamespaceRoute
Expand All @@ -182,6 +208,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ToolsRouteImport
parentRoute: typeof rootRouteImport
}
'/setup-mcp': {
id: '/setup-mcp'
path: '/setup-mcp'
fullPath: '/setup-mcp'
preLoaderRoute: typeof SetupMcpRouteImport
parentRoute: typeof rootRouteImport
}
'/secrets': {
id: '/secrets'
path: '/secrets'
Expand All @@ -203,6 +236,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof OrgRouteImport
parentRoute: typeof rootRouteImport
}
'/create-org': {
id: '/create-org'
path: '/create-org'
fullPath: '/create-org'
preLoaderRoute: typeof CreateOrgRouteImport
parentRoute: typeof rootRouteImport
}
'/connections': {
id: '/connections'
path: '/connections'
Expand Down Expand Up @@ -260,9 +300,11 @@ const rootRouteChildren: RootRouteChildren = {
ApiKeysRoute: ApiKeysRoute,
BillingRoute: BillingRoute,
ConnectionsRoute: ConnectionsRoute,
CreateOrgRoute: CreateOrgRoute,
OrgRoute: OrgRoute,
PoliciesRoute: PoliciesRoute,
SecretsRoute: SecretsRoute,
SetupMcpRoute: SetupMcpRoute,
ToolsRoute: ToolsRoute,
BillingPlansRoute: BillingPlansRoute,
SourcesNamespaceRoute: SourcesNamespaceRoute,
Expand Down
30 changes: 27 additions & 3 deletions apps/cloud/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import React from "react";
import * as Sentry from "@sentry/react";
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
useLocation,
useNavigate,
} from "@tanstack/react-router";
import { AutumnProvider } from "autumn-js/react";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
Expand All @@ -13,10 +20,11 @@ import { plugins as clientPlugins } from "virtual:executor/plugins-client";
import { AuthProvider, useAuth } from "../web/auth";
import { SupportOptions } from "../web/components/support-options";
import { LoginPage } from "../web/pages/login";
import { OnboardingPage } from "../web/pages/onboarding";
import { Shell } from "../web/shell";
import appCss from "@executor-js/react/globals.css?url";

const ONBOARDING_PATHS = new Set(["/create-org", "/setup-mcp"]);

if (typeof window !== "undefined" && import.meta.env.VITE_PUBLIC_SENTRY_DSN) {
Sentry.init({
dsn: import.meta.env.VITE_PUBLIC_SENTRY_DSN,
Expand Down Expand Up @@ -198,6 +206,18 @@ function ShellErrorFallback() {

function AuthGate() {
const auth = useAuth();
const location = useLocation();
const navigate = useNavigate();
const isOnboardingRoute = ONBOARDING_PATHS.has(location.pathname);

const needsOrgRedirect =
auth.status === "authenticated" && auth.organization == null && !isOnboardingRoute;

React.useEffect(() => {
if (needsOrgRedirect) {
void navigate({ to: "/create-org", replace: true });
}
}, [needsOrgRedirect, navigate]);

if (auth.status === "loading") {
return <ShellSkeleton />;
Expand All @@ -207,8 +227,12 @@ function AuthGate() {
return <LoginPage />;
}

if (isOnboardingRoute) {
return <Outlet />;
}

if (auth.organization == null) {
return <OnboardingPage />;
return <ShellSkeleton />;
}

return (
Expand Down
6 changes: 6 additions & 0 deletions apps/cloud/src/routes/create-org.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { CreateOrgPage } from "../web/pages/create-org";

export const Route = createFileRoute("/create-org")({
component: CreateOrgPage,
});
6 changes: 6 additions & 0 deletions apps/cloud/src/routes/setup-mcp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { SetupMcpPage } from "../web/pages/setup-mcp";

export const Route = createFileRoute("/setup-mcp")({
component: SetupMcpPage,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useAtomValue, useAtomSet } from "@effect/atom-react";
import { useNavigate } from "@tanstack/react-router";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import * as Exit from "effect/Exit";
import { authWriteKeys } from "@executor-js/react/api/reactivity-keys";
Expand Down Expand Up @@ -39,8 +40,9 @@ const formatRelativeTime = (iso: string): string => {
return `${Math.floor(days / 365)}y ago`;
};

export const OnboardingPage = () => {
export const CreateOrgPage = () => {
const auth = useAuth();
const navigate = useNavigate();
const invitationsResult = useAtomValue(pendingInvitationsAtom);
const doAccept = useAtomSet(acceptInvitation, { mode: "promiseExit" });

Expand Down Expand Up @@ -75,7 +77,9 @@ export const OnboardingPage = () => {

const form = useCreateOrganizationForm({
defaultName: suggestedName,
onSuccess: () => {},
onSuccess: () => {
void navigate({ to: "/setup-mcp" });
},
});

const isLoading =
Expand All @@ -93,6 +97,9 @@ export const OnboardingPage = () => {
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
<header className="flex flex-col gap-2">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Step 1 of 2
</p>
<h1 className="font-serif text-3xl">
{isLoading
? "Loading"
Expand Down
78 changes: 78 additions & 0 deletions apps/cloud/src/web/pages/setup-mcp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useEffect, useState } from "react";
import { useNavigate } from "@tanstack/react-router";
import { Button } from "@executor-js/react/components/button";
import { CodeBlock } from "@executor-js/react/components/code-block";
import { CopyButton } from "@executor-js/react/components/copy-button";

const buildInstallCommand = (endpoint: string): string =>
`npx add-mcp ${endpoint} --transport http --name executor`;

export const SetupMcpPage = () => {
const navigate = useNavigate();
const [origin, setOrigin] = useState<string | null>(null);

useEffect(() => {
setOrigin(window.location.origin);
}, []);

const endpoint = origin ? `${origin}/mcp` : "";
const command = endpoint ? buildInstallCommand(endpoint) : "";

return (
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
<div className="mx-auto flex w-full max-w-lg flex-col gap-6">
<header className="flex flex-col gap-2">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Step 2 of 2
</p>
<h1 className="font-serif text-3xl">Connect your MCP client</h1>
<p className="text-sm text-muted-foreground">
Executor exposes your sources, secrets, and tools to any MCP-compatible agent. Copy the
URL into your client, or run the install command.
</p>
</header>

<section aria-label="MCP server URL" className="flex flex-col gap-2">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
MCP server URL
</p>
<div className="flex items-center gap-2 rounded-md border border-border bg-card/60 px-3 py-2">
<span className="min-w-0 flex-1 truncate font-mono text-sm text-foreground/90">
{endpoint || "…"}
</span>
{endpoint && <CopyButton value={endpoint} />}
</div>
<p className="text-xs text-muted-foreground">Paste this into your MCP client config.</p>
</section>

<div className="relative flex items-center">
<div className="h-px flex-1 bg-border" />
<span className="px-3 text-xs uppercase tracking-wider text-muted-foreground">or</span>
<div className="h-px flex-1 bg-border" />
</div>

<section aria-label="Install command" className="flex flex-col gap-2">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Install command
</p>
<CodeBlock code={command} lang="bash" />
<p className="text-xs text-muted-foreground">Adds the server to a supported agent.</p>
</section>

<div className="flex items-center justify-between gap-3">
{/* oxlint-disable-next-line react/forbid-elements */}
<button
type="button"
onClick={() => void navigate({ to: "/" })}
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
Skip for now
</button>
<Button size="sm" onClick={() => void navigate({ to: "/" })}>
Continue to app
</Button>
</div>
</div>
</div>
);
};
Loading