From 0c1e9760a6e23bf7f3a8649f2e1f5af137d83eba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 3 Aug 2026 10:45:50 +0000 Subject: [PATCH] Add organization onboarding wizard in console Unlisted /organizations/:id/onboarding flow with SCIM, access review, agent, MCP, and congrats steps. Learn panels, session-only deferrals, hideSidebar layout, and redirect after create org. Document behavior in contrib/claude/onboarding.md. Signed-off-by: Cursor Agent Co-authored-by: Bryan FRIMIN --- AGENTS.md | 1 + .../iam/organizations/NewOrganizationPage.tsx | 2 +- .../onboarding/ScimOnboardingDoPanel.tsx | 133 ++++++++ .../onboarding/onboardingIamQueries.ts | 50 +++ .../onboarding/OnboardingLayoutLoader.tsx | 61 ++++ .../onboarding/OnboardingPage.tsx | 288 ++++++++++++++++++ .../onboarding/OnboardingPageLoader.tsx | 69 +++++ .../AccessReviewOnboardingDoPanel.tsx | 254 +++++++++++++++ .../_components/AgentOnboardingDoPanel.tsx | 96 ++++++ .../_components/CongratsOnboardingStep.tsx | 104 +++++++ .../_components/McpOnboardingDoPanel.tsx | 71 +++++ .../_components/OnboardingLearnPanel.tsx | 62 ++++ .../_components/OnboardingStepActions.tsx | 96 ++++++ .../_components/OnboardingStepper.tsx | 110 +++++++ .../illustrations/OnboardingIllustrations.tsx | 100 ++++++ .../_lib/OnboardingSessionContext.tsx | 73 +++++ .../onboarding/_lib/onboardingSteps.ts | 73 +++++ .../onboarding/_locales/en-US.json | 89 ++++++ .../onboarding/_locales/fr-FR.json | 89 ++++++ .../pages/organizations/onboarding/routes.ts | 42 +++ apps/console/src/routes.tsx | 2 + contrib/claude/onboarding.md | 58 ++++ 22 files changed, 1922 insertions(+), 1 deletion(-) create mode 100644 apps/console/src/pages/iam/organizations/onboarding/ScimOnboardingDoPanel.tsx create mode 100644 apps/console/src/pages/iam/organizations/onboarding/onboardingIamQueries.ts create mode 100644 apps/console/src/pages/organizations/onboarding/OnboardingLayoutLoader.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/OnboardingPage.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/OnboardingPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/AccessReviewOnboardingDoPanel.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/AgentOnboardingDoPanel.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/CongratsOnboardingStep.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/McpOnboardingDoPanel.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/OnboardingLearnPanel.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/OnboardingStepActions.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/OnboardingStepper.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_components/illustrations/OnboardingIllustrations.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_lib/OnboardingSessionContext.tsx create mode 100644 apps/console/src/pages/organizations/onboarding/_lib/onboardingSteps.ts create mode 100644 apps/console/src/pages/organizations/onboarding/_locales/en-US.json create mode 100644 apps/console/src/pages/organizations/onboarding/_locales/fr-FR.json create mode 100644 apps/console/src/pages/organizations/onboarding/routes.ts create mode 100644 contrib/claude/onboarding.md diff --git a/AGENTS.md b/AGENTS.md index ae9ed7197b..2b87ec2afa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,4 +42,5 @@ Detailed guides for specific subsystems live in `contrib/claude/`: - [`contrib/claude/release/README.md`](contrib/claude/release/README.md) — Release process (per-track version bump, changelog, tag, push) - [`contrib/claude/sandbox.md`](contrib/claude/sandbox.md) — Lima sandbox environments (create, manage, access services) - [`contrib/claude/n8n.md`](contrib/claude/n8n.md) — n8n community node (resources, operations, GraphQL helpers) +- [`contrib/claude/onboarding.md`](contrib/claude/onboarding.md) — Console organization onboarding wizard (unlisted route, steps, persistence) - [`contrib/claude/skills.md`](contrib/claude/skills.md) — Agent skills package (`@probo/skills`, compliance workflows, Probo MCP) diff --git a/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx b/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx index 573845cc08..6d50542d6f 100644 --- a/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx +++ b/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx @@ -83,7 +83,7 @@ function NewOrganizationPageInner() { } const org = r.createOrganization!.organization; - void navigate(`/organizations/${org!.id}`); + void navigate(`/organizations/${org!.id}/onboarding?welcome=1`); toast({ title: t("common.success"), description: t("newOrganizationPage.messages.created"), variant: "success", diff --git a/apps/console/src/pages/iam/organizations/onboarding/ScimOnboardingDoPanel.tsx b/apps/console/src/pages/iam/organizations/onboarding/ScimOnboardingDoPanel.tsx new file mode 100644 index 0000000000..c05fd65e67 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/onboarding/ScimOnboardingDoPanel.tsx @@ -0,0 +1,133 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Link } from "@probo/ui"; +import { Suspense, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { usePreloadedQuery, useQueryLoader } from "react-relay"; + +import type { onboardingIamQueriesScimOnboardingDoPanelQuery } from "#/__generated__/iam/onboardingIamQueriesScimOnboardingDoPanelQuery.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { IAMRelayProvider } from "#/providers/IAMRelayProvider"; +import { + OnboardingStepActions, + OnboardingStepDoCard, +} from "#/pages/organizations/onboarding/_components/OnboardingStepActions"; + +import { ConnectorList } from "../settings/_components/ConnectorList"; +import { SCIMConfiguration } from "../settings/_components/SCIMConfiguration"; +import { scimOnboardingDoPanelQuery } from "./onboardingIamQueries"; + +type ScimOnboardingDoPanelInnerProps = { + onContinue: () => void; + onDefer: () => void; + queryRef: NonNullable< + ReturnType< + typeof useQueryLoader + >[0] + >; +}; + +function ScimOnboardingDoPanelInner(props: ScimOnboardingDoPanelInnerProps & { + onScimUpdated?: () => void; +}) { + const { onContinue, onDefer, onScimUpdated, queryRef } = props; + const { t } = useTranslation("organizations/onboarding"); + const organizationId = useOrganizationId(); + + const { organization } = usePreloadedQuery( + scimOnboardingDoPanelQuery, + queryRef, + ); + if (organization.__typename !== "Organization") { + throw new Error("invalid organization node"); + } + + const complete = !!organization.scimConfiguration?.id; + + useEffect(() => { + if (complete) { + onScimUpdated?.(); + } + }, [complete, onScimUpdated]); + + const settingsHref = `/organizations/${organizationId}/settings/scim`; + + return ( + + {t("actions.openInSettings")} + + )} + /> + )} + > +
+ +
+

{t("steps.scim.manualScimHeading")}

+ +
+
+
+ ); +} + +export function ScimOnboardingDoPanel(props: { + onContinue: () => void; + onDefer: () => void; + onScimUpdated?: () => void; +}) { + const { onContinue, onDefer, onScimUpdated } = props; + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader( + scimOnboardingDoPanelQuery, + ); + + useEffect(() => { + loadQuery({ organizationId }, { fetchPolicy: "store-and-network" }); + }, [loadQuery, organizationId]); + + if (!queryRef) { + return null; + } + + return ( + + + + + + ); +} diff --git a/apps/console/src/pages/iam/organizations/onboarding/onboardingIamQueries.ts b/apps/console/src/pages/iam/organizations/onboarding/onboardingIamQueries.ts new file mode 100644 index 0000000000..911b9bfb6d --- /dev/null +++ b/apps/console/src/pages/iam/organizations/onboarding/onboardingIamQueries.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { graphql } from "relay-runtime"; + +export const onboardingIamStatusQuery = graphql` + query onboardingIamQueriesOnboardingIamStatusQuery($organizationId: ID!) { + organization: node(id: $organizationId) @required(action: THROW) { + __typename + ... on Organization { + scimConfiguration { + id + } + } + } + } +`; + +export const scimOnboardingDoPanelQuery = graphql` + query onboardingIamQueriesScimOnboardingDoPanelQuery($organizationId: ID!) { + organization: node(id: $organizationId) @required(action: THROW) { + __typename + ... on Organization { + id + scimConfiguration { + id + } + ...ConnectorListFragment + ...SCIMConfigurationFragment + } + } + } +`; diff --git a/apps/console/src/pages/organizations/onboarding/OnboardingLayoutLoader.tsx b/apps/console/src/pages/organizations/onboarding/OnboardingLayoutLoader.tsx new file mode 100644 index 0000000000..2c606edf9e --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/OnboardingLayoutLoader.tsx @@ -0,0 +1,61 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Skeleton } from "@probo/ui"; +import { Suspense, useEffect } from "react"; +import { useQueryLoader } from "react-relay"; + +import type { ViewerMembershipLayoutQuery } from "#/__generated__/iam/ViewerMembershipLayoutQuery.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { IAMRelayProvider } from "#/providers/IAMRelayProvider"; + +import { + ViewerMembershipLayout, + viewerMembershipLayoutQuery, +} from "../../iam/organizations/ViewerMembershipLayout"; + +function OnboardingLayoutQueryLoader() { + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader( + viewerMembershipLayoutQuery, + ); + + useEffect(() => { + loadQuery({ organizationId, hideSidebar: true }); + }, [organizationId, loadQuery]); + + if (!queryRef) { + return ; + } + + return ( + }> + + + ); +} + +export default function OnboardingLayoutLoader() { + return ( + + + + ); +} diff --git a/apps/console/src/pages/organizations/onboarding/OnboardingPage.tsx b/apps/console/src/pages/organizations/onboarding/OnboardingPage.tsx new file mode 100644 index 0000000000..af33bfa51f --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/OnboardingPage.tsx @@ -0,0 +1,288 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { usePageTitle } from "@probo/hooks"; +import { Role } from "@probo/helpers"; +import { PageHeader } from "@probo/ui"; +import { useCallback, useEffect, useMemo, use } from "react"; +import { useTranslation } from "react-i18next"; +import { + type PreloadedQuery, + graphql, + useFragment, + usePreloadedQuery, +} from "react-relay"; +import { Navigate, useSearchParams } from "react-router"; + +import type { onboardingIamQueriesOnboardingIamStatusQuery } from "#/__generated__/iam/onboardingIamQueriesOnboardingIamStatusQuery.graphql"; +import type { OnboardingPageQuery } from "#/__generated__/core/OnboardingPageQuery.graphql"; +import type { OnboardingPageStatusFragment$key } from "#/__generated__/core/OnboardingPageStatusFragment.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { CurrentUser } from "#/providers/CurrentUser"; + +import { AccessReviewOnboardingDoPanel } from "./_components/AccessReviewOnboardingDoPanel"; +import { AgentOnboardingDoPanel } from "./_components/AgentOnboardingDoPanel"; +import { CongratsOnboardingStep } from "./_components/CongratsOnboardingStep"; +import { + AccessReviewOnboardingIllustration, + AgentOnboardingIllustration, + McpOnboardingIllustration, + ScimOnboardingIllustration, +} from "./_components/illustrations/OnboardingIllustrations"; +import { McpOnboardingDoPanel } from "./_components/McpOnboardingDoPanel"; +import { OnboardingLearnPanel } from "./_components/OnboardingLearnPanel"; +import { OnboardingStepper } from "./_components/OnboardingStepper"; +import { ScimOnboardingDoPanel } from "#/pages/iam/organizations/onboarding/ScimOnboardingDoPanel"; +import { OnboardingSessionProvider, useOnboardingSession } from "./_lib/OnboardingSessionContext"; +import { + allIntegrationsComplete, + firstIncompleteStep, + isOnboardingStepId, + ONBOARDING_STEP_IDS, + type OnboardingCompletion, + type OnboardingStepId, +} from "./_lib/onboardingSteps"; +import type { IntegrationStepId } from "./_lib/onboardingSteps"; + +export const onboardingPageQuery = graphql` + query OnboardingPageQuery($organizationId: ID!) { + accessReviewDrivers { + ...AddAccessReviewSourceDialogConnectorProviderInfoFragment + } + organization: node(id: $organizationId) @required(action: THROW) { + __typename + ... on Organization { + ...OnboardingPageStatusFragment + ...AccessReviewOnboardingDoPanelFragment + ...AgentOnboardingDoPanelFragment + } + } + } +`; + +const onboardingPageStatusFragment = graphql` + fragment OnboardingPageStatusFragment on Organization { + accessReviewSources(first: 50) { + edges { + node { + id + } + } + } + devices(first: 1) { + edges { + node { + id + } + } + } + } +`; + +import { onboardingIamStatusQuery } from "#/pages/iam/organizations/onboarding/onboardingIamQueries";(current: OnboardingStepId): OnboardingStepId { + const index = ONBOARDING_STEP_IDS.indexOf(current); + return ONBOARDING_STEP_IDS[Math.min(index + 1, ONBOARDING_STEP_IDS.length - 1)]!; +} + +function resolveInitialStep( + completion: OnboardingCompletion, + deferred: ReadonlySet, + welcome: boolean, +): OnboardingStepId { + if (welcome) return "scim"; + if (allIntegrationsComplete(completion)) return "congrats"; + return firstIncompleteStep(completion, deferred); +} + +function OnboardingPageInner(props: { + coreQueryRef: PreloadedQuery; + iamQueryRef: PreloadedQuery; + refetchIamStatus: () => void; +}) { + const { coreQueryRef, iamQueryRef, refetchIamStatus } = props; + const { t } = useTranslation("organizations/onboarding"); + const role = use(CurrentUser).role; + const organizationId = useOrganizationId(); + const [searchParams, setSearchParams] = useSearchParams(); + const { clearDeferred, deferStep, deferredSteps } = useOnboardingSession(); + + const { accessReviewDrivers, organization } = usePreloadedQuery( + onboardingPageQuery, + coreQueryRef, + ); + const iamData = usePreloadedQuery( + onboardingIamStatusQuery, + iamQueryRef, + ); + + if (organization.__typename !== "Organization") { + throw new Error("invalid organization node"); + } + if (iamData.organization.__typename !== "Organization") { + throw new Error("invalid organization node"); + } + + const orgStatus = useFragment( + onboardingPageStatusFragment, + organization, + ); + + usePageTitle(t("pageTitle")); + + const completion = useMemo( + () => ({ + scim: !!iamData.organization.scimConfiguration?.id, + accessReview: orgStatus.accessReviewSources.edges.length > 0, + agent: orgStatus.devices.edges.length > 0, + }), + [ + iamData.organization.scimConfiguration?.id, + orgStatus.accessReviewSources.edges.length, + orgStatus.devices.edges.length, + ], + ); + + useEffect(() => { + for (const stepId of ["scim", "accessReview", "agent"] as const) { + if (completion[stepId]) { + clearDeferred(stepId); + } + } + }, [clearDeferred, completion]); + + const welcome = searchParams.get("welcome") === "1"; + const stepParam = searchParams.get("step"); + const activeStep: OnboardingStepId = isOnboardingStepId(stepParam) + ? stepParam + : resolveInitialStep(completion, deferredSteps, welcome); + + const refetchIamScim = useCallback(() => { + refetchIamStatus(); + }, [refetchIamStatus]); + + const goToStep = useCallback( + (stepId: OnboardingStepId) => { + const params = new URLSearchParams(searchParams); + params.set("step", stepId); + params.delete("welcome"); + setSearchParams(params); + }, + [searchParams, setSearchParams], + ); + + const handleContinue = useCallback(() => { + goToStep(nextStep(activeStep)); + }, [activeStep, goToStep]); + + const handleDefer = useCallback(() => { + if (activeStep === "scim" || activeStep === "accessReview" || activeStep === "agent") { + deferStep(activeStep); + } + goToStep(nextStep(activeStep)); + }, [activeStep, deferStep, goToStep]); + + if (role === Role.EMPLOYEE) { + return ; + } + if (role === Role.AUDITOR) { + return ; + } + + return ( +
+ + + + {activeStep === "congrats" + ? ( + + ) + : ( +
+ + : activeStep === "accessReview" + ? + : activeStep === "agent" + ? + : + } + /> +
+ {activeStep === "scim" && ( + + )} + {activeStep === "accessReview" && ( + + )} + {activeStep === "agent" && ( + + )} + {activeStep === "mcp" && ( + + )} +
+
+ )} +
+ ); +} + +export function OnboardingPage(props: { + coreQueryRef: PreloadedQuery; + iamQueryRef: PreloadedQuery; + refetchIamStatus: () => void; +}) { + return ( + + + + ); +} diff --git a/apps/console/src/pages/organizations/onboarding/OnboardingPageLoader.tsx b/apps/console/src/pages/organizations/onboarding/OnboardingPageLoader.tsx new file mode 100644 index 0000000000..f4b5ce7578 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/OnboardingPageLoader.tsx @@ -0,0 +1,69 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { useCallback, useEffect } from "react"; +import { useQueryLoader } from "react-relay"; + +import type { onboardingIamQueriesOnboardingIamStatusQuery } from "#/__generated__/iam/onboardingIamQueriesOnboardingIamStatusQuery.graphql"; +import type { OnboardingPageQuery } from "#/__generated__/core/OnboardingPageQuery.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { IAMRelayProvider } from "#/providers/IAMRelayProvider"; + +import { + OnboardingPage, + onboardingPageQuery, +} from "./OnboardingPage"; +import { onboardingIamStatusQuery } from "#/pages/iam/organizations/onboarding/onboardingIamQueries"; + +export default function OnboardingPageLoader() { + const organizationId = useOrganizationId(); + const [coreQueryRef, loadCoreQuery] = useQueryLoader( + onboardingPageQuery, + ); + const [iamQueryRef, loadIamQuery] = useQueryLoader( + onboardingIamStatusQuery, + ); + + useEffect(() => { + loadCoreQuery({ organizationId }); + }, [loadCoreQuery, organizationId]); + + useEffect(() => { + loadIamQuery({ organizationId }); + }, [loadIamQuery, organizationId]); + + const refetchIamStatus = useCallback(() => { + loadIamQuery({ organizationId }, { fetchPolicy: "network-only" }); + }, [loadIamQuery, organizationId]); + + if (!coreQueryRef || !iamQueryRef) { + return null; + } + + return ( + + + + ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/AccessReviewOnboardingDoPanel.tsx b/apps/console/src/pages/organizations/onboarding/_components/AccessReviewOnboardingDoPanel.tsx new file mode 100644 index 0000000000..6739e759cb --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/AccessReviewOnboardingDoPanel.tsx @@ -0,0 +1,254 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { formatError } from "@probo/helpers"; +import { Button, IconPlusLarge, Link, useToast } from "@probo/ui"; +import { useEffect, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { graphql, useFragment, useMutation } from "react-relay"; + +import type { AccessReviewOnboardingDoPanelFragment$key } from "#/__generated__/core/AccessReviewOnboardingDoPanelFragment.graphql"; +import type { accessReviewSourceMutationsCreateMutation } from "#/__generated__/core/accessReviewSourceMutationsCreateMutation.graphql"; +import type { AddAccessReviewSourceDialogConnectorProviderInfoFragment$key } from "#/__generated__/core/AddAccessReviewSourceDialogConnectorProviderInfoFragment.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { AddAccessReviewSourceDialog, addAccessReviewSourceDialogConnectorProviderInfoFragment } from "#/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog"; +import { createAccessReviewSourceMutation } from "#/pages/organizations/access-reviews/dialogs/accessReviewSourceMutations"; +import { useSearchParams } from "react-router"; + +import { + OnboardingStepActions, + OnboardingStepDoCard, +} from "./OnboardingStepActions"; + +const accessReviewOnboardingDoPanelFragment = graphql` + fragment AccessReviewOnboardingDoPanelFragment on Organization { + canCreateSource: permission(action: "access-review:source:create") + accessReviewSources(first: 50) { + __id + edges { + node { + id + connectorId + connector { + provider + } + } + } + } + } +`; + +function clearOAuthCallbackParams(params: URLSearchParams) { + params.delete("connector_id"); + params.delete("provider"); + params.delete("error"); + return params; +} + +type Props = { + accessReviewDrivers: AddAccessReviewSourceDialogConnectorProviderInfoFragment$key; + onContinue: () => void; + onDefer: () => void; + organizationFKey: AccessReviewOnboardingDoPanelFragment$key; +}; + +export function AccessReviewOnboardingDoPanel(props: Props) { + const { accessReviewDrivers, onContinue, onDefer, organizationFKey } = props; + const { t } = useTranslation("organizations/onboarding"); + const { t: tSources } = useTranslation(); + const { toast } = useToast(); + const organizationId = useOrganizationId(); + const [searchParams, setSearchParams] = useSearchParams(); + const processedConnectorIdRef = useRef(null); + + const organization = useFragment( + accessReviewOnboardingDoPanelFragment, + organizationFKey, + ); + + const connectorProviderInfos = useFragment( + addAccessReviewSourceDialogConnectorProviderInfoFragment, + accessReviewDrivers, + ); + + const accessReviewSources = organization.accessReviewSources; + const complete = accessReviewSources.edges.length > 0; + + const existingSourceProviders = useMemo( + () => + accessReviewSources.edges + .map(edge => edge.node.connector?.provider) + .filter((p): p is NonNullable => p != null), + [accessReviewSources.edges], + ); + + const [createAccessReviewSource, isCreatingSource] + = useMutation( + createAccessReviewSourceMutation, + ); + + const callbackConnectorId = searchParams.get("connector_id"); + const callbackProvider = searchParams.get("provider"); + const callbackError = searchParams.get("error"); + const hasSourceForCallback = !!callbackConnectorId + && accessReviewSources.edges.some( + edge => edge.node.connectorId === callbackConnectorId, + ); + + useEffect(() => { + if (!callbackConnectorId) return; + + if (hasSourceForCallback) { + const createInFlight + = processedConnectorIdRef.current === callbackConnectorId; + if (callbackError && !createInFlight) { + toast({ + title: tSources("accessReviewSourcesTab.messages.error"), + description: callbackError, + variant: "error", + }); + } + if (!createInFlight) { + processedConnectorIdRef.current = null; + setSearchParams(clearOAuthCallbackParams, { replace: true }); + } + return; + } + + if (processedConnectorIdRef.current === callbackConnectorId || isCreatingSource) { + return; + } + processedConnectorIdRef.current = callbackConnectorId; + + const providerInfo = callbackProvider + ? connectorProviderInfos.find(p => p.provider === callbackProvider) + : null; + const sourceName = providerInfo?.displayName ?? callbackProvider ?? "Source"; + + createAccessReviewSource({ + variables: { + input: { + organizationId, + connectorId: callbackConnectorId, + name: sourceName, + csvData: null, + }, + connections: [accessReviewSources.__id], + }, + onCompleted(_, errors) { + if (errors?.length) { + processedConnectorIdRef.current = null; + setSearchParams(clearOAuthCallbackParams, { replace: true }); + toast({ + title: tSources("accessReviewSourcesTab.messages.error"), + description: formatError( + tSources("accessReviewSourcesTab.errors.create"), + errors, + ), + variant: "error", + }); + return; + } + if (callbackError) { + toast({ + title: tSources("accessReviewSourcesTab.messages.error"), + description: callbackError, + variant: "error", + }); + } else { + toast({ + title: tSources("accessReviewSourcesTab.messages.success"), + description: tSources("accessReviewSourcesTab.messages.created"), + variant: "success", + }); + } + processedConnectorIdRef.current = null; + setSearchParams(clearOAuthCallbackParams, { replace: true }); + }, + onError(error) { + processedConnectorIdRef.current = null; + setSearchParams(clearOAuthCallbackParams, { replace: true }); + toast({ + title: tSources("accessReviewSourcesTab.messages.error"), + description: formatError( + tSources("accessReviewSourcesTab.errors.create"), + error, + ), + variant: "error", + }); + }, + }); + }, [ + accessReviewSources.__id, + accessReviewSources.edges, + callbackConnectorId, + callbackError, + callbackProvider, + connectorProviderInfos, + createAccessReviewSource, + hasSourceForCallback, + isCreatingSource, + organizationId, + setSearchParams, + tSources, + toast, + ]); + + const settingsHref = `/organizations/${organizationId}/access-reviews/sources`; + + return ( + + {t("actions.openInSettings")} + + )} + /> + )} + > + {organization.canCreateSource + ? ( + + + + ) + : ( +

+ {t("steps.accessReview.noPermission")} +

+ )} +
+ ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/AgentOnboardingDoPanel.tsx b/apps/console/src/pages/organizations/onboarding/_components/AgentOnboardingDoPanel.tsx new file mode 100644 index 0000000000..26bee00402 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/AgentOnboardingDoPanel.tsx @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Button, IconPlusLarge, Link } from "@probo/ui"; +import { useTranslation } from "react-i18next"; +import { graphql, useFragment } from "react-relay"; + +import type { AgentOnboardingDoPanelFragment$key } from "#/__generated__/core/AgentOnboardingDoPanelFragment.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { CreateDeviceDialog } from "#/pages/organizations/devices/dialogs/CreateDeviceDialog"; + +import { + OnboardingStepActions, + OnboardingStepDoCard, +} from "./OnboardingStepActions"; + +const agentOnboardingDoPanelFragment = graphql` + fragment AgentOnboardingDoPanelFragment on Organization { + canCreateDevice: permission(action: "itam:device:create") + devices(first: 1) { + edges { + node { + id + } + } + } + } +`; + +type Props = { + onContinue: () => void; + onDefer: () => void; + organizationFKey: AgentOnboardingDoPanelFragment$key; +}; + +export function AgentOnboardingDoPanel(props: Props) { + const { onContinue, onDefer, organizationFKey } = props; + const { t } = useTranslation("organizations/onboarding"); + const organizationId = useOrganizationId(); + + const organization = useFragment( + agentOnboardingDoPanelFragment, + organizationFKey, + ); + + const complete = organization.devices.edges.length > 0; + const settingsHref = `/organizations/${organizationId}/devices`; + + return ( + + {t("actions.openInSettings")} + + )} + /> + )} + > +

{t("steps.agent.instructions")}

+ {organization.canCreateDevice + ? ( + + + + ) + : ( +

{t("steps.agent.noPermission")}

+ )} +
+ ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/CongratsOnboardingStep.tsx b/apps/console/src/pages/organizations/onboarding/_components/CongratsOnboardingStep.tsx new file mode 100644 index 0000000000..72e4d3a40c --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/CongratsOnboardingStep.tsx @@ -0,0 +1,104 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { ButtonLink, Link } from "@probo/ui"; +import { useTranslation } from "react-i18next"; + +import type { OnboardingCompletion } from "../_lib/onboardingSteps"; +import { INTEGRATION_STEP_IDS } from "../_lib/onboardingSteps"; +import type { IntegrationStepId } from "../_lib/onboardingSteps"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; + +import { CongratsOnboardingIllustration } from "./illustrations/OnboardingIllustrations"; +import { OnboardingStepDoCard } from "./OnboardingStepActions"; + +type Props = { + completion: OnboardingCompletion; + deferredSteps: ReadonlySet; +}; + +export function CongratsOnboardingStep(props: Props) { + const { completion, deferredSteps } = props; + const { t } = useTranslation("organizations/onboarding"); + const organizationId = useOrganizationId(); + + const pendingSteps = INTEGRATION_STEP_IDS.filter( + stepId => !completion[stepId], + ); + + const settingsPaths: Record = { + scim: `/organizations/${organizationId}/settings/scim`, + accessReview: `/organizations/${organizationId}/access-reviews/sources`, + agent: `/organizations/${organizationId}/devices`, + }; + + return ( +
+
+ +
+
+

+ {t("steps.congrats.title")} +

+

{t("steps.congrats.description")}

+
+ + {pendingSteps.length > 0 && ( + } + > +
    + {pendingSteps.map((stepId) => { + const deferred = deferredSteps.has(stepId); + return ( +
  • + + {t(`stepper.${stepId}`)} + {deferred && ( + + {" "} + — + {" "} + {t("status.later")} + + )} + + + {t("actions.finishLater")} + +
  • + ); + })} +
+
+ )} + + + {t("steps.congrats.goToConsole")} + +

{t("steps.congrats.closeHint")}

+
+ ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/McpOnboardingDoPanel.tsx b/apps/console/src/pages/organizations/onboarding/_components/McpOnboardingDoPanel.tsx new file mode 100644 index 0000000000..42dc3bff55 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/McpOnboardingDoPanel.tsx @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Anchor, Card } from "@probo/ui"; +import { useTranslation } from "react-i18next"; + +import { + OnboardingStepActions, + OnboardingStepDoCard, +} from "./OnboardingStepActions"; + +type Props = { + onContinue: () => void; + onDefer: () => void; +}; + +export function McpOnboardingDoPanel(props: Props) { + const { onContinue, onDefer } = props; + const { t } = useTranslation("organizations/onboarding"); + + const mcpUrl = `${window.location.origin}/mcp/v1`; + + return ( + + )} + > +
    +
  1. {t("steps.mcp.steps.oauth")}
  2. +
  3. {t("steps.mcp.steps.url")}
  4. +
  5. {t("steps.mcp.steps.skills")}
  6. +
+ +

{t("steps.mcp.endpointLabel")}

+ {mcpUrl} +
+

+ + {t("steps.mcp.docsLink")} + +

+
+ ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/OnboardingLearnPanel.tsx b/apps/console/src/pages/organizations/onboarding/_components/OnboardingLearnPanel.tsx new file mode 100644 index 0000000000..9da95dcf41 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/OnboardingLearnPanel.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +import type { OnboardingStepId } from "../_lib/onboardingSteps"; + +type OnboardingLearnPanelProps = { + stepId: Exclude; + illustration: ReactNode; +}; + +export function OnboardingLearnPanel(props: OnboardingLearnPanelProps) { + const { stepId, illustration } = props; + const { t } = useTranslation("organizations/onboarding"); + + const bullets = t(`steps.${stepId}.bullets`, { + returnObjects: true, + }) as string[]; + + return ( +
+
+ {illustration} +
+
+

+ {t(`steps.${stepId}.learnTitle`)} +

+

+ {t(`steps.${stepId}.purpose`)} +

+
    + {bullets.map(bullet => ( +
  • {bullet}
  • + ))} +
+
+
+ ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/OnboardingStepActions.tsx b/apps/console/src/pages/organizations/onboarding/_components/OnboardingStepActions.tsx new file mode 100644 index 0000000000..343c3fdfb5 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/OnboardingStepActions.tsx @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Badge, Button } from "@probo/ui"; +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +type OnboardingStepActionsProps = { + continueLabel?: string; + continueDisabled?: boolean; + onContinue: () => void; + onDefer?: () => void; + showDefer?: boolean; + settingsLink?: ReactNode; +}; + +export function OnboardingStepActions(props: OnboardingStepActionsProps) { + const { + continueDisabled = false, + continueLabel, + onContinue, + onDefer, + settingsLink, + showDefer = true, + } = props; + const { t } = useTranslation("organizations/onboarding"); + + return ( +
+ {settingsLink} +
+ {showDefer && onDefer + ? ( + + ) + : } + +
+
+ ); +} + +export function OnboardingStepDoCard(props: { + title: string; + description?: string; + complete?: boolean; + children: ReactNode; + actions: ReactNode; +}) { + const { actions, children, complete, description, title } = props; + const { t } = useTranslation("organizations/onboarding"); + + return ( +
+
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ {complete && ( + {t("status.complete")} + )} +
+ {children} + {actions} +
+ ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/OnboardingStepper.tsx b/apps/console/src/pages/organizations/onboarding/_components/OnboardingStepper.tsx new file mode 100644 index 0000000000..11047a2b92 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/OnboardingStepper.tsx @@ -0,0 +1,110 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { IconCheckmark1 } from "@probo/ui"; +import { useTranslation } from "react-i18next"; + +import { + INTEGRATION_STEP_IDS, + ONBOARDING_STEP_IDS, + type OnboardingCompletion, + type OnboardingStepId, +} from "../_lib/onboardingSteps"; +import type { IntegrationStepId } from "../_lib/onboardingSteps"; + +type OnboardingStepperProps = { + activeStep: OnboardingStepId; + completion: OnboardingCompletion; + deferredSteps: ReadonlySet; + onStepClick: (stepId: OnboardingStepId) => void; +}; + +function stepLabelKey(stepId: OnboardingStepId): string { + if (stepId === "congrats") return "stepper.congrats"; + return `stepper.${stepId}`; +} + +export function OnboardingStepper(props: OnboardingStepperProps) { + const { activeStep, completion, deferredSteps, onStepClick } = props; + const { t } = useTranslation("organizations/onboarding"); + + const steps = ONBOARDING_STEP_IDS; + + return ( + + ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_components/illustrations/OnboardingIllustrations.tsx b/apps/console/src/pages/organizations/onboarding/_components/illustrations/OnboardingIllustrations.tsx new file mode 100644 index 0000000000..4141904fef --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_components/illustrations/OnboardingIllustrations.tsx @@ -0,0 +1,100 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +export function ScimOnboardingIllustration() { + return ( + + + IdP + + + + Probo + + + + + ); +} + +export function AccessReviewOnboardingIllustration() { + return ( + + + + + + + + Review + + ); +} + +export function AgentOnboardingIllustration() { + return ( + + + + + + + + ); +} + +export function McpOnboardingIllustration() { + return ( + + + IDE + + + MCP + + ); +} + +export function CongratsOnboardingIllustration() { + return ( + + + + + ); +} diff --git a/apps/console/src/pages/organizations/onboarding/_lib/OnboardingSessionContext.tsx b/apps/console/src/pages/organizations/onboarding/_lib/OnboardingSessionContext.tsx new file mode 100644 index 0000000000..4b69c31400 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_lib/OnboardingSessionContext.tsx @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { ReactNode } from "react"; +import { createContext, useCallback, useContext, useMemo, useState } from "react"; + +import type { IntegrationStepId } from "./onboardingSteps"; + +type OnboardingSessionContextValue = { + deferredSteps: ReadonlySet; + deferStep: (stepId: IntegrationStepId) => void; + clearDeferred: (stepId: IntegrationStepId) => void; +}; + +const OnboardingSessionContext = createContext( + null, +); + +export function OnboardingSessionProvider(props: { children: ReactNode }) { + const { children } = props; + const [deferredSteps, setDeferredSteps] = useState>( + () => new Set(), + ); + + const deferStep = useCallback((stepId: IntegrationStepId) => { + setDeferredSteps(prev => new Set(prev).add(stepId)); + }, []); + + const clearDeferred = useCallback((stepId: IntegrationStepId) => { + setDeferredSteps((prev) => { + if (!prev.has(stepId)) return prev; + const next = new Set(prev); + next.delete(stepId); + return next; + }); + }, []); + + const value = useMemo( + () => ({ deferredSteps, deferStep, clearDeferred }), + [clearDeferred, deferStep, deferredSteps], + ); + + return ( + + {children} + + ); +} + +export function useOnboardingSession(): OnboardingSessionContextValue { + const ctx = useContext(OnboardingSessionContext); + if (!ctx) { + throw new Error("useOnboardingSession must be used within OnboardingSessionProvider"); + } + return ctx; +} diff --git a/apps/console/src/pages/organizations/onboarding/_lib/onboardingSteps.ts b/apps/console/src/pages/organizations/onboarding/_lib/onboardingSteps.ts new file mode 100644 index 0000000000..72cfa23f36 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_lib/onboardingSteps.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +export const ONBOARDING_STEP_IDS = [ + "scim", + "accessReview", + "agent", + "mcp", + "congrats", +] as const; + +export type OnboardingStepId = (typeof ONBOARDING_STEP_IDS)[number]; + +export const INTEGRATION_STEP_IDS = [ + "scim", + "accessReview", + "agent", +] as const satisfies readonly OnboardingStepId[]; + +export type IntegrationStepId = (typeof INTEGRATION_STEP_IDS)[number]; + +export function isOnboardingStepId(value: string | null): value is OnboardingStepId { + return value !== null && (ONBOARDING_STEP_IDS as readonly string[]).includes(value); +} + +export type OnboardingCompletion = { + scim: boolean; + accessReview: boolean; + agent: boolean; +}; + +export function stepIsComplete( + stepId: IntegrationStepId, + completion: OnboardingCompletion, +): boolean { + return completion[stepId]; +} + +export function firstIncompleteStep( + completion: OnboardingCompletion, + deferred: ReadonlySet, +): OnboardingStepId { + for (const stepId of INTEGRATION_STEP_IDS) { + if (!stepIsComplete(stepId, completion) && !deferred.has(stepId)) { + return stepId; + } + } + if (!deferred.has("scim") && !completion.scim) return "scim"; + if (!deferred.has("accessReview") && !completion.accessReview) return "accessReview"; + if (!deferred.has("agent") && !completion.agent) return "agent"; + return "mcp"; +} + +export function allIntegrationsComplete(completion: OnboardingCompletion): boolean { + return completion.scim && completion.accessReview && completion.agent; +} diff --git a/apps/console/src/pages/organizations/onboarding/_locales/en-US.json b/apps/console/src/pages/organizations/onboarding/_locales/en-US.json new file mode 100644 index 0000000000..eede17403f --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_locales/en-US.json @@ -0,0 +1,89 @@ +{ + "pageTitle": "Organization onboarding", + "title": "Welcome to Probo", + "description": "Connect your stack in a few steps. You can finish anything later from Settings.", + "stepper": { + "ariaLabel": "Onboarding progress", + "scim": "SCIM", + "accessReview": "Access reviews", + "agent": "Probo agent", + "mcp": "MCP", + "congrats": "Done" + }, + "status": { + "complete": "Connected", + "later": "Later" + }, + "actions": { + "continue": "Continue", + "doLater": "Do this later", + "openInSettings": "Open in Settings", + "finishLater": "Finish in Settings" + }, + "steps": { + "scim": { + "learnTitle": "Sync people from your identity provider", + "purpose": "SCIM keeps Probo aligned with joiners, movers, and leavers in your IdP. Accurate people data powers access reviews and ownership across the platform.", + "bullets": [ + "Automatic user and group provisioning", + "Fewer manual invites and stale accounts", + "Consistent baseline for IAM and compliance workflows" + ], + "doTitle": "Connect SCIM", + "doDescription": "Connect Google Workspace, Microsoft 365, or enable manual SCIM credentials.", + "manualScimHeading": "Manual SCIM token" + }, + "accessReview": { + "learnTitle": "Connect systems for access reviews", + "purpose": "Access review connectors pull account and permission data from your SaaS and identity tools so campaigns reflect real access.", + "bullets": [ + "See who has access in each connected app", + "Run periodic access review campaigns", + "Evidence for auditors without spreadsheet hunts" + ], + "doTitle": "Add a connector", + "doDescription": "Choose a provider and authorize Probo to read access data.", + "addConnector": "Add connector", + "noPermission": "You do not have permission to add sources. Ask an organization admin, or open Settings later." + }, + "agent": { + "learnTitle": "Enroll devices with the Probo agent", + "purpose": "The Probo agent reports endpoint posture (OS, encryption, and related signals) so you can manage fleet compliance alongside your cloud controls.", + "bullets": [ + "Link employees to enrolled devices", + "Monitor posture from the Devices page", + "Support endpoint requirements in your frameworks" + ], + "doTitle": "Register a device", + "doDescription": "Create an enrollment and install the agent on a machine.", + "instructions": "Register a device to get an enrollment token, then install probo-agent on the endpoint using the instructions shown in the dialog.", + "registerDevice": "Register device", + "noPermission": "You do not have permission to register devices. Ask an organization admin, or open Devices in Settings later." + }, + "mcp": { + "learnTitle": "Connect AI tools with Probo MCP", + "purpose": "The Probo MCP server lets Cursor, Claude Code, and other agents call your org data securely via OAuth—no static API keys in config files.", + "bullets": [ + "OAuth to your Probo instance", + "Use compliance skills and workflows from your IDE", + "Same policies as the rest of the product" + ], + "doTitle": "Install MCP in your editor", + "doDescription": "Point your MCP client at this instance and complete OAuth when prompted.", + "steps": { + "oauth": "In Cursor or your agent, add an MCP server and choose OAuth when connecting.", + "url": "Use the endpoint below (HTTP transport).", + "skills": "Optional: install @probo/skills for ready-made compliance workflows." + }, + "endpointLabel": "MCP endpoint", + "docsLink": "Probo skills and MCP setup guide" + }, + "congrats": { + "title": "You are all set for now", + "description": "Thanks for walking through onboarding. You can close this tab or jump into the console anytime.", + "pendingTitle": "Still open", + "goToConsole": "Go to tasks", + "closeHint": "Safe to close this page—you can return to onboarding anytime with the same link." + } + } +} diff --git a/apps/console/src/pages/organizations/onboarding/_locales/fr-FR.json b/apps/console/src/pages/organizations/onboarding/_locales/fr-FR.json new file mode 100644 index 0000000000..2a3d5823f0 --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/_locales/fr-FR.json @@ -0,0 +1,89 @@ +{ + "pageTitle": "Intégration de l’organisation", + "title": "Bienvenue sur Probo", + "description": "Connectez votre stack en quelques étapes. Vous pourrez terminer plus tard depuis les paramètres.", + "stepper": { + "ariaLabel": "Progression de l’intégration", + "scim": "SCIM", + "accessReview": "Revues d’accès", + "agent": "Agent Probo", + "mcp": "MCP", + "congrats": "Terminé" + }, + "status": { + "complete": "Connecté", + "later": "Plus tard" + }, + "actions": { + "continue": "Continuer", + "doLater": "Plus tard", + "openInSettings": "Ouvrir dans les paramètres", + "finishLater": "Terminer dans les paramètres" + }, + "steps": { + "scim": { + "learnTitle": "Synchroniser les personnes depuis votre IdP", + "purpose": "SCIM aligne Probo avec les arrivées, mouvements et départs dans votre fournisseur d’identité. Des données personnes fiables alimentent les revues d’accès et la gouvernance.", + "bullets": [ + "Approvisionnement automatique des utilisateurs et groupes", + "Moins d’invitations manuelles et de comptes obsolètes", + "Base cohérente pour l’IAM et la conformité" + ], + "doTitle": "Connecter SCIM", + "doDescription": "Connectez Google Workspace, Microsoft 365 ou activez un jeton SCIM manuel.", + "manualScimHeading": "Jeton SCIM manuel" + }, + "accessReview": { + "learnTitle": "Connecter les systèmes pour les revues d’accès", + "purpose": "Les connecteurs remontent comptes et droits depuis vos SaaS et outils d’identité pour des campagnes fidèles à la réalité.", + "bullets": [ + "Voir qui a accès dans chaque application connectée", + "Lancer des campagnes de revue périodiques", + "Des preuves pour les auditeurs sans chasse aux tableurs" + ], + "doTitle": "Ajouter un connecteur", + "doDescription": "Choisissez un fournisseur et autorisez Probo à lire les données d’accès.", + "addConnector": "Ajouter un connecteur", + "noPermission": "Vous n’avez pas la permission d’ajouter des sources. Demandez à un admin ou ouvrez les paramètres plus tard." + }, + "agent": { + "learnTitle": "Inscrire des postes avec l’agent Probo", + "purpose": "L’agent Probo remonte la posture des postes (OS, chiffrement, etc.) pour gérer la conformité endpoint avec vos contrôles cloud.", + "bullets": [ + "Lier les employés aux postes inscrits", + "Suivre la posture depuis la page Appareils", + "Soutenir les exigences endpoint de vos référentiels" + ], + "doTitle": "Enregistrer un appareil", + "doDescription": "Créez une inscription et installez l’agent sur une machine.", + "instructions": "Enregistrez un appareil pour obtenir un jeton d’inscription, puis installez probo-agent en suivant les instructions du dialogue.", + "registerDevice": "Enregistrer un appareil", + "noPermission": "Vous n’avez pas la permission d’enregistrer des appareils. Demandez à un admin ou ouvrez Appareils plus tard." + }, + "mcp": { + "learnTitle": "Connecter vos outils IA via MCP Probo", + "purpose": "Le serveur MCP Probo permet à Cursor, Claude Code et d’autres agents d’appeler vos données org via OAuth—sans clé API statique.", + "bullets": [ + "OAuth vers votre instance Probo", + "Skills et workflows conformité depuis l’IDE", + "Les mêmes politiques que le reste du produit" + ], + "doTitle": "Installer MCP dans l’éditeur", + "doDescription": "Pointez votre client MCP vers cette instance et validez OAuth.", + "steps": { + "oauth": "Dans Cursor ou votre agent, ajoutez un serveur MCP et choisissez OAuth.", + "url": "Utilisez l’endpoint ci-dessous (transport HTTP).", + "skills": "Optionnel : installez @probo/skills pour des workflows prêts à l’emploi." + }, + "endpointLabel": "Endpoint MCP", + "docsLink": "Guide skills et MCP Probo" + }, + "congrats": { + "title": "C’est bon pour l’instant", + "description": "Merci d’avoir suivi l’intégration. Vous pouvez fermer cet onglet ou ouvrir la console.", + "pendingTitle": "Encore à faire", + "goToConsole": "Aller aux tâches", + "closeHint": "Vous pouvez fermer cette page—le même lien rouvre l’intégration." + } + } +} diff --git a/apps/console/src/pages/organizations/onboarding/routes.ts b/apps/console/src/pages/organizations/onboarding/routes.ts new file mode 100644 index 0000000000..2cef837dda --- /dev/null +++ b/apps/console/src/pages/organizations/onboarding/routes.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { lazy } from "@probo/react-lazy"; + +import { PageSkeleton } from "#/components/skeletons/PageSkeleton"; + +export const onboardingRoutes = [ + { + path: "onboarding", + Fallback: PageSkeleton, + Component: lazy( + () => import("#/pages/organizations/onboarding/OnboardingLayoutLoader"), + ), + children: [ + { + index: true, + Fallback: PageSkeleton, + Component: lazy( + () => import("#/pages/organizations/onboarding/OnboardingPageLoader"), + ), + }, + ], + }, +]; diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index c9f4515e89..5e05452486 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -37,6 +37,7 @@ import { ViewerLayoutLoading } from "./pages/iam/memberships/ViewerLayoutLoading import { peopleRoutes } from "./pages/iam/organizations/people/routes"; import { compliancePageRoutes } from "./pages/organizations/compliance-page/routes"; import { cookieBannerRoutes } from "./pages/organizations/cookie-banners/routes"; +import { onboardingRoutes } from "./pages/organizations/onboarding/routes"; import { deviceRoutes } from "./pages/organizations/devices/routes"; import { riskRoutes } from "./pages/organizations/risks/routes"; import { thirdPartyRoutes } from "./pages/organizations/third-parties/routes"; @@ -191,6 +192,7 @@ const routes = [ path: "assume", Component: lazy(() => import("./pages/iam/organizations/AssumePageLoader")), }, + ...onboardingRoutes, { path: "employee", ErrorBoundary: OrganizationErrorBoundary, diff --git a/contrib/claude/onboarding.md b/contrib/claude/onboarding.md new file mode 100644 index 0000000000..cd00e4cffd --- /dev/null +++ b/contrib/claude/onboarding.md @@ -0,0 +1,58 @@ +# Organization onboarding wizard (console) + +Unlisted wizard at `/organizations/:organizationId/onboarding` for peer / new +organization setup. Not linked from sidebar or nav; shareable URL plus redirect +after **Create organization**. + +## Steps + +| # | Step ID | Purpose | Done (GraphQL) | +|---|---------|---------|----------------| +| 1 | `scim` | IdP provisioning via SCIM | `organization.scimConfiguration` exists | +| 2 | `accessReview` | Access review connectors | ≥1 `accessReviewSources` edge | +| 3 | `agent` | Probo agent / device fleet | ≥1 `devices` edge | +| 4 | `mcp` | MCP setup instructions | Never auto; **Continue** always | +| 5 | `congrats` | Completion | Reach step 5 in flow | + +## UX + +- **Learn** panel: title, purpose, bullets, SVG/CSS illustration (`prefers-reduced-motion`). +- **Do** panel: embedded existing flows (SCIM connectors, add access review source, create device). +- **Do this later** on steps 1–4: session-only deferral; advances to next step. +- **Continue** on MCP without checkbox. +- **Congrats**: done vs still-to-do from integration state; optional link to console tasks. + +## Persistence + +- No backend onboarding model. +- **Done** = live integration data (refetch after mutations). +- **Deferred** = in-memory React context only (no `localStorage`); lost on tab close or refresh. + +## Navigation + +- Post-create: navigate to `/organizations/:id/onboarding`. +- Step in URL: `?step=scim|accessReview|agent|mcp|congrats` (browser back). +- Initial step: first incomplete integration step, or **congrats** if all complete. +- `?welcome=1` after create: start at step 1 even if empty (optional; default first gap). + +## Access + +- Authenticated org member required. +- **Employee** → redirect to `employee` portal. +- **Auditor** → redirect to `measures`. +- Missing permissions: show learn panel + deep link to settings; still allow **Do this later**. + +## Layout + +- `hideSidebar` org layout (focused wizard), same pattern as employee layout. + +## Deep links (finish later) + +- SCIM → `/organizations/:id/settings/scim` +- Access review → `/organizations/:id/access-reviews/sources` +- Devices → `/organizations/:id/devices` +- MCP → instance `${origin}` + `/mcp/v1`, skills / IDE docs + +## i18n + +- Namespace `organizations/onboarding` → `pages/organizations/onboarding/_locales/*.json`