diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f6d81f2bc9c..94522182c9b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -32,6 +32,7 @@ import { CompanySettings } from "./pages/CompanySettings"; import { CompanyEnvironments } from "./pages/CompanyEnvironments"; import { CloudUpstream } from "./pages/CloudUpstream"; import { CloudUpstreamUxLab } from "./pages/CloudUpstreamUxLab"; +import { BootstrapSetupUxLab } from "./pages/BootstrapSetupUxLab"; import { CompanySettingsPluginPage } from "./pages/CompanySettingsPluginPage"; import { CompanyAccess, CompanyAccessLegacyRoute } from "./pages/CompanyAccess"; import { CompanyInvites } from "./pages/CompanyInvites"; @@ -284,6 +285,7 @@ export function App() { } /> } /> } /> + } /> }> } /> diff --git a/ui/src/pages/BootstrapSetupUxLab.tsx b/ui/src/pages/BootstrapSetupUxLab.tsx new file mode 100644 index 00000000000..22810dbadd3 --- /dev/null +++ b/ui/src/pages/BootstrapSetupUxLab.tsx @@ -0,0 +1,243 @@ +import type { ReactElement, ReactNode } from "react"; +import { Loader2, ShieldCheck, Terminal, TriangleAlert } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +type LabFixtureKey = + | "signed-out-private" + | "signed-in-private" + | "claiming" + | "claim-error" + | "claim-success" + | "public-invite-only"; + +const FIXTURE_LABELS: Record = { + "signed-out-private": "1 · authenticated/private — signed out (browser claim available)", + "signed-in-private": "2 · authenticated/private — signed in (claim CTA primary)", + claiming: "3 · authenticated/private — claim in flight", + "claim-error": "4 · authenticated/private — claim error (e.g. 409 already claimed)", + "claim-success": "5 · authenticated/private — claim succeeded, redirect pending", + "public-invite-only": "6 · authenticated/public — invite-only (no browser claim)", +}; + +const FIXTURE_ORDER: LabFixtureKey[] = [ + "signed-out-private", + "signed-in-private", + "claiming", + "claim-error", + "claim-success", + "public-invite-only", +]; + +const FALLBACK_COMMAND = "pnpm paperclipai auth bootstrap-ceo"; + +function CliFallback({ hasActiveInvite }: { hasActiveInvite: boolean }) { + return ( +
+
+ + Prefer to finish setup from the host? +
+

+ {hasActiveInvite + ? "A bootstrap invite is already active. Check your Paperclip startup logs for the first‑admin URL, or run this command on the host to rotate it:" + : "Run this command on the host that runs Paperclip to print a one‑time first‑admin invite URL:"} +

+
+{FALLBACK_COMMAND}
+      
+
+ ); +} + +function StateChrome({ children }: { children: ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +function SignedOutPrivate() { + return ( + +

Finish setting up this Paperclip

+

+ No admin has claimed this instance yet. Sign in or create your Paperclip account to become the first + admin from this browser. +

+ + +
+ ); +} + +function SignedInPrivate() { + return ( + +

Finish setting up this Paperclip

+

+ No admin has claimed this instance yet. Claim it now to become the first admin and start onboarding. +

+
+ + + Signed in as jane@appliance.local + +
+

+ Wrong account?{" "} + + Switch account + + . +

+ +
+ ); +} + +function ClaimingPrivate() { + return ( + +

Finish setting up this Paperclip

+

+ No admin has claimed this instance yet. Claim it now to become the first admin and start onboarding. +

+
+ + + Signed in as jane@appliance.local + +
+ +
+ ); +} + +function ClaimErrorPrivate() { + return ( + +

Finish setting up this Paperclip

+

+ No admin has claimed this instance yet. Claim it now to become the first admin and start onboarding. +

+
+ + + Signed in as jane@appliance.local + +
+
+ +
+

Someone else has already claimed this instance.

+

+ Refresh to sign in, or ask the existing admin to invite you from{" "} + Instance settings → Access. +

+
+
+ +
+ ); +} + +function ClaimSuccess() { + return ( + +
+
+ +
+
+

You’re the instance admin

+

+ Setup is complete. Taking you to onboarding to create your first company… +

+
+
+
+ + Redirecting… +
+
+ ); +} + +function PublicInviteOnly() { + return ( + +

This Paperclip is waiting on its first admin

+

+ This instance runs in invite‑only mode. The operator must generate a one‑time first‑admin invite URL + from the host. Once you have the link, open it from this browser to finish setup. +

+ +

+ Browser‑based claim is intentionally disabled in public mode so anyone on the network can’t + promote themselves. +

+
+ ); +} + +const FIXTURE_BODIES: Record = { + "signed-out-private": , + "signed-in-private": , + claiming: , + "claim-error": , + "claim-success": , + "public-invite-only": , +}; + +export function BootstrapSetupUxLab() { + return ( +
+
+
+

UX Lab

+

Bootstrap-pending setup states

+

+ Fixtures for the bootstrap-pending screen in CloudAccessGate. Used + as the UX spec for{" "} + + PAP-10113 + {" "} + and the implementation reference for{" "} + + PAP-10114 + + . The browser claim CTA only appears when{" "} + deploymentMode === "authenticated" and{" "} + deploymentExposure === "private". +

+
+
+
+ {FIXTURE_ORDER.map((key) => ( +
+

+ {FIXTURE_LABELS[key]} +

+
+ {FIXTURE_BODIES[key]} +
+
+ ))} +
+
+ ); +}