489 lines · 19.5 KB · TypeScript
RawDownload
import { Link, Outlet, useLocation, useParams } from "@tanstack/react-router";import { useEffect, useRef, useState, type ReactNode } from "react";import { useAtomValue } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import { BookOpen, ExternalLink } from "lucide-react";import type { Integration } from "@executor-js/sdk/shared";import { integrationsOptimisticAtom } from "../api/atoms";import { trackEvent } from "../api/analytics";import { Button } from "../components/button";import { Skeleton } from "../components/skeleton";import { SidebarUpdateCard } from "../components/update-card";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuLabel,  DropdownMenuTrigger,} from "../components/dropdown-menu";import {  IntegrationFavicon,  integrationInferredUrl,  integrationPresetIconUrl,} from "../components/integration-favicon";import { CommandPalette } from "../components/command-palette";import { useClientPlugins, useIntegrationPlugins } from "@executor-js/sdk/client";import { useAuth } from "./auth-context"; // ---------------------------------------------------------------------------// Shared multiplayer shell (cloud + self-host).//// Provider-neutral: identity comes from the shared `useAuth()` seam. The bits// that genuinely differ per product are injected://   - `onSignOut`     how the session is ended (WorkOS logout vs Better Auth)//   - `orgMenuSlot`   org switcher / create-org (cloud only)//   - `supportSlot`   support dialog button (cloud only)//   - `navItems`      which sections show (e.g. cloud adds Billing)// Everything visual is identical so both products look the same.// --------------------------------------------------------------------------- /** Nav targets are ORG-SCOPE-RELATIVE paths ("/", "/policies", "/admin") — the *  shell prefixes the optional `{-$orgSlug}` segment when rendering, and the *  router's param inheritance keeps the active org's slug in every href. */export type ShellNavItem = { readonly to: string; readonly label: string }; /** Integrations lives at "/", plus the standard tool-management sections. Hosts *  spread this and append their own (e.g. API keys, Organization, Billing). A *  host only adds API keys if it actually serves them in-app (Cloudflare manages *  them in Access, so it omits the link). */export const defaultShellNavItems: ReadonlyArray<ShellNavItem> = [  { to: "/", label: "Integrations" },  { to: "/secrets", label: "Providers" },  { to: "/policies", label: "Policies" },  { to: "/toolkits", label: "Toolkits" },]; /** Canonical public docs (Mintlify). Same-origin on cloud (executor.sh proxies *  `/docs`); the correct external target for self-host / Cloudflare / local, *  whose `/docs` is Swagger UI rather than the product docs. Hosts can override *  via {@link ShellProps.docsUrl}. */export const DEFAULT_DOCS_URL = "https://executor.sh/docs"; // The build version, surfaced in the sidebar footer so self-hosters can see// what they're running (and sanity-check the update card). Undefined on builds// that don't inject it (e.g. cloud) — then the line is hidden.const APP_VERSION = (  import.meta as ImportMeta & { readonly env?: { readonly VITE_APP_VERSION?: string } }).env?.VITE_APP_VERSION; // Scope-relative path -> the route id under the optional org segment// ("/" -> "/{-$orgSlug}", "/policies" -> "/{-$orgSlug}/policies"). Loosely// typed on purpose: host-specific items ("/admin", "/billing") resolve against// the host's own route tree, which this package can't see.const orgScopedTo = (to: string): string => (to === "/" ? "/{-$orgSlug}" : `/{-$orgSlug}${to}`); const normalizePluginPagePath = (path: string): string => {  if (!path || path === "/") return "/";  return path.startsWith("/") ? path : `/${path}`;}; const pluginPageNavPath = (pluginId: string, path: string): string => {  const normalized = normalizePluginPagePath(path);  return normalized === "/" ? `/plugins/${pluginId}/` : `/plugins/${pluginId}${normalized}`;}; /** The pathname with the active org-slug prefix stripped, for active-state *  comparisons against scope-relative paths. */function useScopeRelativePathname(): string {  const pathname = useLocation({ select: (location) => location.pathname });  const params = useParams({ strict: false }) as { orgSlug?: string };  const orgSlug = params.orgSlug;  if (orgSlug && (pathname === `/${orgSlug}` || pathname.startsWith(`/${orgSlug}/`))) {    return pathname.slice(orgSlug.length + 1) || "/";  }  return pathname;} export interface ShellProps {  /** End the session. Cloud POSTs its logout path; self-host calls Better Auth. */  readonly onSignOut: () => void | Promise<void>;  /** Nav sections; defaults to {@link defaultShellNavItems}. */  readonly navItems?: ReadonlyArray<ShellNavItem>;  /** Injected into the account dropdown — cloud's org switcher / create-org. */  readonly orgMenuSlot?: ReactNode;  /** Injected support button above the account footer (cloud). */  readonly supportSlot?: ReactNode;  /** Docs link target; defaults to {@link DEFAULT_DOCS_URL}. Opens in a new tab. */  readonly docsUrl?: string;  /** Replaces the routed `<Outlet />` (the org-slug gate's in-shell 404). */  readonly content?: ReactNode;} // ── Brand ──────────────────────────────────────────────────────────────── function Brand(props: { onNavigate?: () => void }) {  return (    <Link to="/{-$orgSlug}" onClick={props.onNavigate} className="flex items-center gap-1.5">      <span className="font-mono text-sm font-medium tracking-tight text-foreground">executor</span>      <span className="rounded-sm bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary">        Beta      </span>    </Link>  );} // ── NavItem ────────────────────────────────────────────────────────────── function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) {  return (    <Link      to={orgScopedTo(props.to)}      onClick={props.onNavigate}      className={[        "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",        props.active          ? "bg-sidebar-active text-foreground font-medium"          : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground",      ].join(" ")}    >      {props.label}    </Link>  );} // ── DocsLink ─────────────────────────────────────────────────────────────── /** External link to the documentation. Lives in the sidebar footer (always *  visible, outside the scrollable nav) so docs are reachable from inside the *  app, not only the marketing site. */function DocsLink(props: { href: string; onNavigate?: () => void }) {  return (    <a      href={props.href}      target="_blank"      rel="noopener noreferrer"      onClick={() => {        trackEvent("docs_opened", { surface: "sidebar" });        props.onNavigate?.();      }}      className="group flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm text-sidebar-foreground transition-colors hover:bg-sidebar-active/60 hover:text-foreground"    >      <BookOpen className="size-4 shrink-0" />      <span className="flex-1">Docs</span>      <ExternalLink className="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />    </a>  );} // ── IntegrationList ─────────────────────────────────────────────────────────── // `pathname` is scope-relative (org-slug prefix already stripped).function IntegrationList(props: { pathname: string; onNavigate?: () => void }) {  const integrations = useAtomValue(integrationsOptimisticAtom);  const integrationPlugins = useIntegrationPlugins();   return AsyncResult.match(integrations, {    onInitial: () => (      <div className="flex flex-col gap-1 px-2.5 py-1">        {[80, 65, 72, 58, 68].map((w, i) => (          <div key={i} className="flex items-center gap-2 rounded-md py-1.5">            <Skeleton className="size-3.5 shrink-0 rounded" />            <Skeleton className="h-3" style={{ width: `${w}%` }} />          </div>        ))}      </div>    ),    onFailure: () => (      <div className="px-2.5 py-2 text-xs text-muted-foreground">No integrations yet</div>    ),    onSuccess: ({ value }) =>      value.length === 0 ? (        <div className="px-2.5 py-2 text-sm leading-relaxed text-muted-foreground">          No integrations yet        </div>      ) : (        <div className="flex flex-col gap-px">          {value.map((integration: Integration) => {            const slug = String(integration.slug);            const name = integration.name || slug;            const detailPath = `/integrations/${slug}`;            const active =              props.pathname === detailPath || props.pathname.startsWith(`${detailPath}/`);            return (              <Link                key={slug}                to="/{-$orgSlug}/integrations/$namespace"                params={{ namespace: slug }}                onClick={props.onNavigate}                className={[                  "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs transition-colors",                  active                    ? "bg-sidebar-active text-foreground font-medium"                    : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground",                ].join(" ")}              >                <IntegrationFavicon                  icon={integrationPresetIconUrl(                    { id: slug, kind: integration.kind, name, url: integration.displayUrl },                    integrationPlugins,                  )}                  sourceId={slug}                  url={                    integration.displayUrl ??                    integrationInferredUrl({ id: slug, name }) ??                    undefined                  }                />                <span className="flex-1 truncate">{name}</span>              </Link>            );          })}        </div>      ),  });} // ── Avatar / initials ────────────────────────────────────────────────────── function initialsFor(name: string | null, email: string) {  if (name) {    return name      .split(" ")      .map((n) => n[0])      .join("")      .slice(0, 2)      .toUpperCase();  }  return email[0]!.toUpperCase();} function Avatar(props: { url: string | null; name: string | null; email: string }) {  if (props.url) {    return <img src={props.url} alt="" className="size-7 shrink-0 rounded-full" />;  }  return (    <div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">      {initialsFor(props.name, props.email)}    </div>  );} // ── UserFooter ────────────────────────────────────────────────────────── function UserFooter(props: Pick<ShellProps, "onSignOut" | "orgMenuSlot">) {  const auth = useAuth();  if (auth.status !== "authenticated") return null;   return (    <div className="shrink-0 border-t border-sidebar-border px-3 py-2.5">      <DropdownMenu>        <DropdownMenuTrigger asChild>          <Button            type="button"            variant="ghost"            className="flex h-auto w-full items-center justify-start gap-2.5 rounded-md px-1 py-1 text-left hover:bg-sidebar-active/60"          >            <Avatar url={auth.user.avatarUrl} name={auth.user.name} email={auth.user.email} />            <div className="min-w-0 flex-1">              <p className="truncate text-xs font-medium text-foreground">                {auth.user.name ?? auth.user.email}              </p>              {auth.organization && (                <p className="truncate text-xs text-muted-foreground">{auth.organization.name}</p>              )}            </div>            <svg              viewBox="0 0 16 16"              fill="none"              className="size-3.5 shrink-0 text-muted-foreground"            >              <path                d="M4 6l4 4 4-4"                stroke="currentColor"                strokeWidth="1.3"                strokeLinecap="round"                strokeLinejoin="round"              />            </svg>          </Button>        </DropdownMenuTrigger>        <DropdownMenuContent align="start" side="top" className="w-64">          {props.orgMenuSlot}          <DropdownMenuLabel className="text-xs font-normal text-muted-foreground">            Signed in as          </DropdownMenuLabel>          <DropdownMenuItem disabled className="gap-2 text-xs opacity-100">            <Avatar url={auth.user.avatarUrl} name={auth.user.name} email={auth.user.email} />            <div className="min-w-0 flex-1">              <p className="truncate font-medium text-foreground">                {auth.user.name ?? auth.user.email}              </p>              {auth.user.name && (                <p className="truncate text-muted-foreground">{auth.user.email}</p>              )}            </div>          </DropdownMenuItem>          <DropdownMenuItem            className="text-xs text-destructive focus:text-destructive"            onClick={() => void props.onSignOut()}          >            Sign out          </DropdownMenuItem>        </DropdownMenuContent>      </DropdownMenu>    </div>  );} // ── SidebarContent ─────────────────────────────────────────────────────── function SidebarContent(  props: ShellProps & { pathname: string; onNavigate?: () => void; showBrand?: boolean },) {  const plugins = useClientPlugins();  const pluginNavItems = plugins.flatMap((plugin) =>    (plugin.pages ?? []).flatMap((page) =>      page.nav        ? [            {              to: pluginPageNavPath(plugin.id, page.path),              label: page.nav.label,            },          ]        : [],    ),  );  const navItems = [...(props.navItems ?? defaultShellNavItems), ...pluginNavItems];  return (    <>      {props.showBrand !== false && (        <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4">          <Brand onNavigate={props.onNavigate} />        </div>      )}       <nav className="flex flex-1 flex-col overflow-y-auto p-2">        {navItems.map((item) => (          <NavItem            key={item.to}            to={item.to}            label={item.label}            active={item.to === "/" ? props.pathname === "/" : props.pathname.startsWith(item.to)}            onNavigate={props.onNavigate}          />        ))}         <div className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground">          <span>Integrations</span>        </div>         <IntegrationList pathname={props.pathname} onNavigate={props.onNavigate} />      </nav>       <SidebarUpdateCard />       <div className="shrink-0 border-t border-sidebar-border p-2">        <DocsLink href={props.docsUrl ?? DEFAULT_DOCS_URL} onNavigate={props.onNavigate} />        {APP_VERSION && (          <p className="px-2.5 pt-1.5 font-mono text-[11px] text-muted-foreground tabular-nums">            v{APP_VERSION}          </p>        )}      </div>       {props.supportSlot && <div className="shrink-0 px-2 pb-2">{props.supportSlot}</div>}       <UserFooter onSignOut={props.onSignOut} orgMenuSlot={props.orgMenuSlot} />    </>  );} // ── Shell ───────────────────────────────────────────────────────────────── export function Shell(props: ShellProps) {  const pathname = useScopeRelativePathname();  const lastPathname = useRef(pathname);  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);  if (lastPathname.current !== pathname) {    lastPathname.current = pathname;    if (mobileSidebarOpen) setMobileSidebarOpen(false);  }   useEffect(() => {    if (!mobileSidebarOpen) return;    const prev = document.body.style.overflow;    document.body.style.overflow = "hidden";    return () => {      document.body.style.overflow = prev;    };  }, [mobileSidebarOpen]);   return (    <div className="flex h-screen overflow-hidden">      <CommandPalette />      {/* Desktop sidebar */}      <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">        <SidebarContent {...props} pathname={pathname} />      </aside>       {/* Mobile sidebar overlay */}      {mobileSidebarOpen && (        <div className="fixed inset-0 z-50 flex md:hidden">          {/* oxlint-disable-next-line react/forbid-elements */}          <button            type="button"            aria-label="Close navigation"            className="absolute inset-0 bg-black/45 backdrop-blur-[1px]"            onClick={() => setMobileSidebarOpen(false)}          />          <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl">            <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4">              <Brand onNavigate={() => setMobileSidebarOpen(false)} />              <Button                variant="ghost"                size="icon-sm"                type="button"                aria-label="Close navigation"                onClick={() => setMobileSidebarOpen(false)}                className="text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground"              >                <svg viewBox="0 0 16 16" className="size-3.5">                  <path                    d="M3 3l10 10M13 3L3 13"                    stroke="currentColor"                    strokeWidth="1.4"                    strokeLinecap="round"                  />                </svg>              </Button>            </div>            <SidebarContent              {...props}              pathname={pathname}              onNavigate={() => setMobileSidebarOpen(false)}              showBrand={false}            />          </div>        </div>      )}       {/* Main content */}      <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden">        {/* Mobile top bar */}        <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">          <Button            variant="outline"            size="icon-sm"            type="button"            aria-label="Open navigation"            onClick={() => setMobileSidebarOpen(true)}            className="bg-card hover:bg-accent/50"          >            <svg viewBox="0 0 16 16" className="size-4">              <path                d="M2 4h12M2 8h12M2 12h12"                stroke="currentColor"                strokeWidth="1.2"                strokeLinecap="round"              />            </svg>          </Button>          <Brand />          <div className="w-8 shrink-0" />        </div>         {props.content ?? <Outlet />}      </main>    </div>  );}