From 1a30b374d630af9df39908739fa0d78062ff9c3d Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:57:34 +0100 Subject: [PATCH] refactor(ui): remove the onboarding system and product tours (#13176) --- .github/workflows/ui-tests.yml | 4 - AGENTS.md | 6 - skills/prowler-tour/SKILL.md | 99 ---- skills/prowler-tour/assets/tour-template.ts | 51 -- .../prowler-tour/references/output-format.md | 31 -- .../references/tours-architecture.md | 44 -- ui/AGENTS.md | 66 ++- ui/actions/invitations/invitation.adapter.ts | 2 +- ui/actions/invitations/invitation.test.ts | 4 +- ui/actions/invitations/invitation.ts | 2 +- ui/actions/invitations/roles.ts | 2 +- .../_components/scan-list-table.test.tsx | 23 +- .../_components/scan-list-table.tsx | 3 - .../_hooks/use-attack-path-scans.ts | 21 +- .../attack-paths-page.harness.ts | 11 +- .../query-builder/attack-paths-page.tsx | 138 +----- ui/app/(prowler)/attack-paths/layout.tsx | 6 +- .../_components/compliance-page-tabs.tsx | 2 +- ui/app/(prowler)/compliance/page.tsx | 49 +- ui/app/(prowler)/findings/loading.tsx | 6 +- ui/app/(prowler)/findings/page.tsx | 17 +- ui/app/(prowler)/layout.tsx | 48 +- ui/app/(prowler)/providers/page.tsx | 16 +- ui/app/(prowler)/scans/page.test.tsx | 57 +-- ui/app/(prowler)/scans/page.tsx | 25 +- ui/changelog.d/remove-onboarding.changed.md | 1 + .../compliance-overview-grid.test.tsx | 85 ---- .../compliance/compliance-overview-grid.tsx | 79 +-- ui/components/findings/findings-filters.tsx | 2 +- .../table/findings-group-table.test.tsx | 118 +---- .../findings/table/findings-group-table.tsx | 38 +- .../table/inline-resource-container.tsx | 2 - .../forms/send-invitation-form.test.tsx | 4 +- .../workflow/forms/send-invitation-form.tsx | 4 +- .../workflow/forms/use-send-invitation.ts | 4 +- .../layout/nav-bar/navbar-client.test.tsx | 280 +---------- .../layout/nav-bar/navbar-client.tsx | 99 +--- ui/components/layout/nav-bar/navbar.tsx | 12 +- .../onboarding-checkpoint-dialog.test.tsx | 109 ----- ...oarding-checkpoint-watcher-invite.test.tsx | 132 ----- .../onboarding-checkpoint-watcher.test.tsx | 117 ----- .../__tests__/onboarding-gate.test.tsx | 320 ------------- .../__tests__/onboarding-invite-step.test.tsx | 229 --------- .../onboarding-sequence-banner.logic.test.ts | 63 --- .../onboarding-sequence-banner.test.tsx | 193 -------- .../onboarding-trigger.decision.test.ts | 164 ------- .../__tests__/onboarding-trigger.test.tsx | 318 ------------- .../onboarding/__tests__/page-ready.test.tsx | 30 -- ui/components/onboarding/index.ts | 5 - .../onboarding-checkpoint-dialog.tsx | 38 -- .../onboarding-checkpoint-watcher.tsx | 99 ---- .../onboarding/onboarding-invite-dialog.tsx | 68 --- .../onboarding/onboarding-invite-step.tsx | 49 -- .../onboarding-sequence-banner.logic.ts | 32 -- .../onboarding/onboarding-sequence-banner.tsx | 100 ---- .../onboarding/onboarding-trigger.logic.ts | 48 -- .../onboarding/onboarding-trigger.tsx | 171 ------- ui/components/onboarding/page-ready.tsx | 25 - .../providers/add-provider-button.tsx | 4 +- .../providers/first-run-redirect.test.tsx | 266 +++++++++++ .../first-run-redirect.tsx} | 62 +-- .../providers/no-providers-added.tsx | 3 - .../providers-accounts-view.test.tsx | 50 -- .../providers/providers-accounts-view.tsx | 46 +- .../use-provider-wizard-controller.test.tsx | 86 +--- .../hooks/use-provider-wizard-controller.ts | 17 - .../wizard/provider-wizard-modal.test.tsx | 26 - .../wizard/provider-wizard-modal.tsx | 18 +- .../steps/aws/aws-connect-step.test.tsx | 6 +- .../wizard/steps/aws/aws-connect-step.tsx | 3 - .../providers/wizard/steps/connect-step.tsx | 6 - .../invite-teammate/invite-teammate-form.tsx | 2 +- .../invite-teammate/invite-teammate-panel.tsx | 2 +- .../invite-teammate/invite-teammate-sent.tsx | 2 +- .../workflow/forms/connect-account-form.tsx | 2 +- ui/components/scans/scans-page-shell.test.tsx | 137 ------ ui/components/scans/scans-page-shell.tsx | 37 +- .../table/__tests__/scan-jobs-table.test.tsx | 77 --- ui/components/scans/table/scan-jobs-table.tsx | 9 - .../breadcrumb-navigation.test.tsx | 19 - .../breadcrumbs/breadcrumb-navigation.tsx | 3 - ui/components/shadcn/button/button.test.tsx | 6 +- .../shadcn/content-layout/content-layout.tsx | 15 +- .../discovery-callout.test.tsx | 83 ---- .../discovery-callout/discovery-callout.tsx | 88 ---- ui/components/shadcn/table/data-table.tsx | 8 - .../side-panel/side-panel-trigger.test.tsx | 122 +---- .../side-panel/side-panel-trigger.tsx | 77 +-- ui/dependency-log.json | 8 - ui/hooks/use-invitation-roles.ts | 2 +- ui/lib/invitations/order-roles.ts | 2 +- ui/lib/onboarding/README.md | 116 ----- .../onboarding/__tests__/flow-route.test.ts | 49 -- .../__tests__/gate-decision.test.ts | 91 ---- ui/lib/onboarding/__tests__/registry.test.ts | 185 ------- ui/lib/onboarding/flow-route.ts | 33 -- ui/lib/onboarding/gate-decision.ts | 21 - ui/lib/onboarding/index.ts | 16 - ui/lib/onboarding/invite-marker.ts | 48 -- ui/lib/onboarding/onboarding-events.ts | 33 -- ui/lib/onboarding/onboarding-types.ts | 22 - ui/lib/onboarding/registry.ts | 83 ---- .../first-run-marker.ts | 16 +- .../tours/__tests__/add-provider.tour.test.ts | 59 --- .../tours/__tests__/attack-paths.tour.test.ts | 120 ----- .../__tests__/explore-findings.tour.test.ts | 89 ---- .../use-driver-tour.lifecycle.test.tsx | 396 --------------- .../tours/__tests__/use-driver-tour.test.tsx | 157 ------ .../__tests__/view-compliance.tour.test.ts | 104 ---- .../__tests__/view-first-scan.tour.test.ts | 99 ---- ui/lib/tours/add-provider.tour.ts | 69 --- ui/lib/tours/attack-paths-empty.tour.ts | 31 -- ui/lib/tours/attack-paths.tour.ts | 98 ---- ui/lib/tours/explore-findings.tour.ts | 74 --- .../tours/store/local-storage-adapter.test.ts | 104 ---- ui/lib/tours/store/local-storage-adapter.ts | 71 --- ui/lib/tours/store/tour-completion-store.ts | 8 - ui/lib/tours/tour-config.test.ts | 56 --- ui/lib/tours/tour-config.ts | 50 -- ui/lib/tours/tour-popover-render.test.tsx | 139 ------ ui/lib/tours/tour-popover-render.tsx | 197 -------- ui/lib/tours/tour-types.ts | 94 ---- ui/lib/tours/use-driver-tour.ts | 450 ------------------ ui/lib/tours/use-tour-completion.ts | 36 -- ui/lib/tours/view-compliance.tour.ts | 85 ---- ui/lib/tours/view-first-scan.tour.ts | 98 ---- ui/package.json | 2 - ui/pnpm-lock.yaml | 8 - ui/scripts/check-tour-alignment.mjs | 190 -------- ui/scripts/check-tour-alignment.test.ts | 37 -- .../__tests__/onboarding-checkpoint.test.ts | 85 ---- ui/store/__tests__/onboarding-replay.test.ts | 62 --- .../__tests__/onboarding-sequence.test.ts | 132 ----- ui/store/__tests__/page-ready.test.ts | 31 -- ui/store/__tests__/side-panel.test.ts | 46 -- ui/store/onboarding-checkpoint.ts | 46 -- ui/store/onboarding-replay.ts | 34 -- ui/store/onboarding-sequence.ts | 71 --- ui/store/page-ready.ts | 24 - ui/store/side-panel.ts | 28 +- ui/styles/globals.css | 3 - ui/styles/tours.css | 86 ---- ui/tests/base-page.ts | 2 +- ui/tests/registry/add-provider-tour-report.md | 16 - ui/tests/registry/validation.md | 3 +- .../{onboarding-invite.ts => invitations.ts} | 1 - ui/vitest.config.ts | 1 - 147 files changed, 446 insertions(+), 8924 deletions(-) delete mode 100644 skills/prowler-tour/SKILL.md delete mode 100644 skills/prowler-tour/assets/tour-template.ts delete mode 100644 skills/prowler-tour/references/output-format.md delete mode 100644 skills/prowler-tour/references/tours-architecture.md create mode 100644 ui/changelog.d/remove-onboarding.changed.md delete mode 100644 ui/components/onboarding/__tests__/onboarding-checkpoint-dialog.test.tsx delete mode 100644 ui/components/onboarding/__tests__/onboarding-checkpoint-watcher-invite.test.tsx delete mode 100644 ui/components/onboarding/__tests__/onboarding-checkpoint-watcher.test.tsx delete mode 100644 ui/components/onboarding/__tests__/onboarding-gate.test.tsx delete mode 100644 ui/components/onboarding/__tests__/onboarding-invite-step.test.tsx delete mode 100644 ui/components/onboarding/__tests__/onboarding-sequence-banner.logic.test.ts delete mode 100644 ui/components/onboarding/__tests__/onboarding-sequence-banner.test.tsx delete mode 100644 ui/components/onboarding/__tests__/onboarding-trigger.decision.test.ts delete mode 100644 ui/components/onboarding/__tests__/onboarding-trigger.test.tsx delete mode 100644 ui/components/onboarding/__tests__/page-ready.test.tsx delete mode 100644 ui/components/onboarding/index.ts delete mode 100644 ui/components/onboarding/onboarding-checkpoint-dialog.tsx delete mode 100644 ui/components/onboarding/onboarding-checkpoint-watcher.tsx delete mode 100644 ui/components/onboarding/onboarding-invite-dialog.tsx delete mode 100644 ui/components/onboarding/onboarding-invite-step.tsx delete mode 100644 ui/components/onboarding/onboarding-sequence-banner.logic.ts delete mode 100644 ui/components/onboarding/onboarding-sequence-banner.tsx delete mode 100644 ui/components/onboarding/onboarding-trigger.logic.ts delete mode 100644 ui/components/onboarding/onboarding-trigger.tsx delete mode 100644 ui/components/onboarding/page-ready.tsx create mode 100644 ui/components/providers/first-run-redirect.test.tsx rename ui/components/{onboarding/onboarding-gate.tsx => providers/first-run-redirect.tsx} (52%) delete mode 100644 ui/components/scans/table/__tests__/scan-jobs-table.test.tsx delete mode 100644 ui/components/shadcn/discovery-callout/discovery-callout.test.tsx delete mode 100644 ui/components/shadcn/discovery-callout/discovery-callout.tsx delete mode 100644 ui/lib/onboarding/README.md delete mode 100644 ui/lib/onboarding/__tests__/flow-route.test.ts delete mode 100644 ui/lib/onboarding/__tests__/gate-decision.test.ts delete mode 100644 ui/lib/onboarding/__tests__/registry.test.ts delete mode 100644 ui/lib/onboarding/flow-route.ts delete mode 100644 ui/lib/onboarding/gate-decision.ts delete mode 100644 ui/lib/onboarding/index.ts delete mode 100644 ui/lib/onboarding/invite-marker.ts delete mode 100644 ui/lib/onboarding/onboarding-events.ts delete mode 100644 ui/lib/onboarding/onboarding-types.ts delete mode 100644 ui/lib/onboarding/registry.ts rename ui/lib/{onboarding => provider-funnel}/first-run-marker.ts (83%) delete mode 100644 ui/lib/tours/__tests__/add-provider.tour.test.ts delete mode 100644 ui/lib/tours/__tests__/attack-paths.tour.test.ts delete mode 100644 ui/lib/tours/__tests__/explore-findings.tour.test.ts delete mode 100644 ui/lib/tours/__tests__/use-driver-tour.lifecycle.test.tsx delete mode 100644 ui/lib/tours/__tests__/use-driver-tour.test.tsx delete mode 100644 ui/lib/tours/__tests__/view-compliance.tour.test.ts delete mode 100644 ui/lib/tours/__tests__/view-first-scan.tour.test.ts delete mode 100644 ui/lib/tours/add-provider.tour.ts delete mode 100644 ui/lib/tours/attack-paths-empty.tour.ts delete mode 100644 ui/lib/tours/attack-paths.tour.ts delete mode 100644 ui/lib/tours/explore-findings.tour.ts delete mode 100644 ui/lib/tours/store/local-storage-adapter.test.ts delete mode 100644 ui/lib/tours/store/local-storage-adapter.ts delete mode 100644 ui/lib/tours/store/tour-completion-store.ts delete mode 100644 ui/lib/tours/tour-config.test.ts delete mode 100644 ui/lib/tours/tour-config.ts delete mode 100644 ui/lib/tours/tour-popover-render.test.tsx delete mode 100644 ui/lib/tours/tour-popover-render.tsx delete mode 100644 ui/lib/tours/tour-types.ts delete mode 100644 ui/lib/tours/use-driver-tour.ts delete mode 100644 ui/lib/tours/use-tour-completion.ts delete mode 100644 ui/lib/tours/view-compliance.tour.ts delete mode 100644 ui/lib/tours/view-first-scan.tour.ts delete mode 100644 ui/scripts/check-tour-alignment.mjs delete mode 100644 ui/scripts/check-tour-alignment.test.ts delete mode 100644 ui/store/__tests__/onboarding-checkpoint.test.ts delete mode 100644 ui/store/__tests__/onboarding-replay.test.ts delete mode 100644 ui/store/__tests__/onboarding-sequence.test.ts delete mode 100644 ui/store/__tests__/page-ready.test.ts delete mode 100644 ui/store/onboarding-checkpoint.ts delete mode 100644 ui/store/onboarding-replay.ts delete mode 100644 ui/store/onboarding-sequence.ts delete mode 100644 ui/store/page-ready.ts delete mode 100644 ui/styles/tours.css delete mode 100644 ui/tests/registry/add-provider-tour-report.md rename ui/types/{onboarding-invite.ts => invitations.ts} (95%) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index c4d9c91368..4e228d814f 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -139,10 +139,6 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' run: pnpm run healthcheck - - name: Check product-tour alignment - if: steps.check-changes.outputs.any_changed == 'true' - run: pnpm run tour:check - - name: Run pnpm audit if: steps.check-changes.outputs.any_changed == 'true' run: pnpm run audit diff --git a/AGENTS.md b/AGENTS.md index 997c4bbaff..2e6f68b123 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,6 @@ Use these skills for detailed patterns on-demand: | `django-migration-psql` | Django migration best practices for PostgreSQL | [SKILL.md](skills/django-migration-psql/SKILL.md) | | `postgresql-indexing` | PostgreSQL indexing, EXPLAIN, monitoring, maintenance | [SKILL.md](skills/postgresql-indexing/SKILL.md) | | `prowler-attack-paths-query` | Create Attack Paths openCypher queries | [SKILL.md](skills/prowler-attack-paths-query/SKILL.md) | -| `prowler-tour` | Keep product-tour definitions aligned with the UI | [SKILL.md](skills/prowler-tour/SKILL.md) | | `gh-aw` | GitHub Agentic Workflows (gh-aw) | [SKILL.md](skills/gh-aw/SKILL.md) | | `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) | @@ -69,12 +68,10 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Adding new providers | `prowler-provider` | | Adding privilege escalation detection queries | `prowler-attack-paths-query` | | Adding services to existing providers | `prowler-provider` | -| Adding, updating, or removing a tour definition (*.tour.ts) | `prowler-tour` | | After creating/modifying a skill | `skill-sync` | | App Router / Server Actions | `nextjs-16` | | Auditing check-to-requirement mappings as a cloud auditor | `prowler-compliance` | | Building AI chat features | `ai-sdk-5` | -| Changing button labels or section headings on a tour-covered page | `prowler-tour` | | Committing changes | `prowler-commit` | | Configuring MCP servers in agentic workflows | `gh-aw` | | Create PR that requires changelog entry | `prowler-changelog` | @@ -94,7 +91,6 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Creating/updating compliance frameworks | `prowler-compliance` | | Debug why a GitHub Actions job is failing | `prowler-ci` | | Debugging gh-aw compilation errors | `gh-aw` | -| Editing a UI file containing data-tour-id attributes | `prowler-tour` | | Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` | | Fixing bug | `tdd` | | Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs) | `prowler-compliance` | @@ -111,8 +107,6 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` | | Refactoring code | `tdd` | | Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` | -| Renaming or removing a data-tour-id attribute value | `prowler-tour` | -| Restructuring routes or layouts covered by a tour | `prowler-tour` | | Review PR requirements: template, title conventions, changelog gate | `prowler-pr` | | Review changelog format and conventions | `prowler-changelog` | | Reviewing JSON:API compliance | `jsonapi` | diff --git a/skills/prowler-tour/SKILL.md b/skills/prowler-tour/SKILL.md deleted file mode 100644 index 8511581a47..0000000000 --- a/skills/prowler-tour/SKILL.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: prowler-tour -description: > - Keeps product-tour definitions aligned with the UI features they describe. - Trigger: When modifying UI components that have associated tours, editing tour - definition files, or renaming data-tour-id attributes. -license: Apache-2.0 -metadata: - author: prowler-cloud - version: "1.0" - scope: [root, ui] - auto_invoke: - - "Editing a UI file containing data-tour-id attributes" - - "Adding, updating, or removing a tour definition (*.tour.ts)" - - "Renaming or removing a data-tour-id attribute value" - - "Changing button labels or section headings on a tour-covered page" - - "Restructuring routes or layouts covered by a tour" -allowed-tools: Read, Glob, Grep ---- - -# prowler-tour - -**Report-only.** This skill never edits tour files or UI files; it inspects -the change, reports drift it finds between tours and the covered UI, and -recommends actions for the developer to apply. - -## Early-exit rule - -Run this check first. Most UI edits are not tour-related — exit cheaply. - -1. Glob `ui/lib/tours/*.tour.ts`. -2. For each tour, check whether any `coversFiles` glob pattern matches any - file in the current change. -3. If no tour matches, respond **exactly**: - - > No tour affected — skipping alignment check - - and exit. Do not proceed to the checklist. -4. If at least one tour matches, continue to "Drift checklist" for that tour. - -## Drift checklist - -For each affected tour, evaluate every item. Skip items that obviously do -not apply, but list explicitly which items were checked. - -1. **Orphan selectors** — every step's `target` (which composes to - `data-tour-id="-"`) must resolve to a real element - in the codebase. Grep `ui/` for the expected attribute value; report - any step whose target is missing. -2. **Renamed selectors** — a `data-tour-id` attribute was edited in this - change. Match it back to any tour step referencing the old value. -3. **Outdated copy** — a popover `title`/`description` references a button - label, heading, or term that no longer exists on the covered page. -4. **Obsolete steps** — a step describes a section, panel, or workflow - that was removed. -5. **Missing steps** — a new feature was added on the covered surface - without a corresponding step (e.g. a new panel, a new primary action, - a new wizard stage). -6. **Reordered flow** — the user's path through the feature changed (e.g. - query builder moved before scan selection) and the step order no - longer reflects it. - -## Version-bump decision tree - -Apply per tour after listing drift: - -- **NO bump** when the change is cosmetic. Examples: fix a typo, soften - copy, rename a `data-tour-id` selector while keeping the same step, - swap one screenshot for another, tighten wording. -- **BUMP `version`** when the user-visible flow changes materially. - Examples: a new step was added or removed; the order changed; an - anchored target was retargeted to a different panel; the tour now - covers a new feature on the surface. - -When in doubt, ask: "Would a user who already saw the previous version -miss something useful by not seeing this one?" If yes, bump. - -## Output format - -When emitting a report, follow the exact structure in -`references/output-format.md`. The structure is mandatory because the -report is consumed downstream and tolerates no field reordering. - -## What this skill MUST NOT do - -- Do not edit `*.tour.ts` files. This skill is report-only. -- Do not edit UI files to add or rename `data-tour-id` attributes. -- Do not invent new tours. Authoring a new tour is a separate, deliberate - decision — the developer makes it, not the skill. -- Do not flag drift in tours whose `coversFiles` do not match any file - in the current change. Stick to the early-exit rule. - -## See also - -- `references/output-format.md` — exact report template (read when - emitting a report). -- `references/tours-architecture.md` — code map for the tour abstraction - under `ui/lib/tours/`. -- `assets/tour-template.ts` — boilerplate for authoring a new `*.tour.ts`. diff --git a/skills/prowler-tour/assets/tour-template.ts b/skills/prowler-tour/assets/tour-template.ts deleted file mode 100644 index cc264a4678..0000000000 --- a/skills/prowler-tour/assets/tour-template.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -- template only; resolves once copied into `ui/lib/tours/` -/** - * Tour template — copy this file to `ui/lib/tours/.tour.ts` and - * fill in the placeholders. See `references/tours-architecture.md` for the - * design context. - * - * Conventions: - * - Declare via `defineTour({...})` (NOT `: TourDefinition`) so TS - * preserves the literal union of `target` values. `useDriverTour` uses - * that union to validate `stepHandlers` keys and `waitForStep` args. - * - `id` is kebab-case and unique across all tours. - * - Anchored steps reference DOM via `data-tour-id="-"`; - * the hook composes the CSS selector automatically. - * - `coversFiles` lists the globs that describe the tour's surface; the - * `prowler-tour` skill consumes this to decide whether to evaluate - * drift on a given change. - * - Material flow changes bump `version`; cosmetic edits do not. - */ -import { - defineTour, - TOUR_STEP_ALIGNMENTS, - TOUR_STEP_SIDES, -} from "@/lib/tours/tour-types"; - -export const yourTour = defineTour({ - id: "your-tour-id", - version: 1, - coversFiles: [ - // List the UI files this tour describes, using globs under `ui/`. - // Example: "ui/app/(prowler)/your-feature/**" - ], - steps: [ - { - // Modal step — no anchor. Use for intros, outros, and any step - // that does not point at a specific DOM element. - title: "Welcome", - description: "Short, plain-English description.", - }, - { - // Anchored step. The hook resolves - // `[data-tour-id="your-tour-id-step-name"]` lazily, so the element - // can be conditionally rendered as long as it exists when the step - // becomes active. - target: "step-name", - side: TOUR_STEP_SIDES.BOTTOM, - align: TOUR_STEP_ALIGNMENTS.START, - title: "Where the action is", - description: "Tell the user what to look at here and why.", - }, - ], -}); diff --git a/skills/prowler-tour/references/output-format.md b/skills/prowler-tour/references/output-format.md deleted file mode 100644 index ecd82c4d8e..0000000000 --- a/skills/prowler-tour/references/output-format.md +++ /dev/null @@ -1,31 +0,0 @@ -# Tour Alignment Report — output format - -The report is consumed downstream. Field names, order, and headings are -load-bearing — do not rename, reorder, or omit them. - -## Template - -```text -## Tour Alignment Report -**Tour:** `@v` -**Files touched:** - -### Drift detected -- - -### Recommended actions -1. - -### Version bump verdict -- — -``` - -## Rules - -- One report per affected tour. If multiple tours are affected, separate - reports with a `---` line. -- If no drift is detected for an affected tour, still emit the report: - put "No drift detected." under "Drift detected" and "None required." - under "Recommended actions". The verdict line is still mandatory. -- The verdict is exactly one of `BUMP` or `NO bump` — see the - version-bump decision tree in `SKILL.md`. diff --git a/skills/prowler-tour/references/tours-architecture.md b/skills/prowler-tour/references/tours-architecture.md deleted file mode 100644 index 9a5c8f49e0..0000000000 --- a/skills/prowler-tour/references/tours-architecture.md +++ /dev/null @@ -1,44 +0,0 @@ -# Tours Architecture - -The product-tour abstraction lives under [`ui/lib/tours/`](../../../ui/lib/tours/). -This skill operates on tour definitions that follow this architecture. - -## Code map - -| File | Purpose | -|---|---| -| `ui/lib/tours/tour-types.ts` | Public type surface: `TourDefinition`, `TourStep`, `TourId`, `TourCompletionRecord`, completion-state const map. Also exports `defineTour(...)` — the required authoring helper that preserves literal step `target`s so `useDriverTour` can type-check `stepHandlers` keys and `waitForStep` arguments. | -| `ui/lib/tours/tour-config.ts` | `baseDriverConfig`, `getDriverConfig(theme, overrides?)`, overlay-color map. | -| `ui/lib/tours/store/tour-completion-store.ts` | Persistence interface — the swap point for future API adapters. | -| `ui/lib/tours/store/local-storage-adapter.ts` | The only adapter in the PoC. Key format: `prowler.tour..v`. | -| `ui/lib/tours/use-driver-tour.ts` | React hook. Initializes driver.js, derives `overlayColor` from `useTheme()`, persists completion. | -| `ui/lib/tours/.tour.ts` | One file per tour. Declared via `defineTour({...})` (not `: TourDefinition`) and imported by the page that opts the user in. | -| `ui/styles/tours.css` | `.driver-popover.prowler-theme` — every color resolved via `var(--...)` from `globals.css`. | - -## Selector convention - -Tour steps anchor via `data-tour-id="-"`. The hook -composes the CSS selector at runtime; tour authors only provide the step -name in `step.target`. Class-based, ID-based, structural selectors are -forbidden — they couple tours to styling decisions that legitimately -change. - -## Identity and versioning - -A tour is `{ id, version }`. The localStorage key composes both. A -**material content change** bumps `version`; cosmetic edits do not. The -decision tree lives in the parent SKILL.md. - -## Persistence scope - -Per-user, cross-tenant. A user who completed `attack-paths@v1` in tenant -A does not see the tour again in tenant B, even if they can access the -feature there. The future `UserTourState` model (documented in -`design.md`, not built) is FK to `User`, not `Membership`. - -## Drift = #1 risk - -Without the maintenance skill + the optional CI gate -(`ui/scripts/check-tour-alignment.mjs`), tours decay silently as the -covered UI evolves. The parent SKILL.md enumerates the six drift -categories the skill checks for. diff --git a/ui/AGENTS.md b/ui/AGENTS.md index 5939d11394..694aa3f469 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -14,47 +14,41 @@ > - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors > - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library > - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks) -> - [`prowler-tour`](../skills/prowler-tour/SKILL.md) - Keep product-tour definitions aligned with the UI ## Auto-invoke Skills When performing these actions, ALWAYS invoke the corresponding skill FIRST: -| Action | Skill | -| ----------------------------------------------------------------- | ------------------- | -| Add changelog entry for a PR or feature | `prowler-changelog` | -| Adding, updating, or removing a tour definition (\*.tour.ts) | `prowler-tour` | -| App Router / Server Actions | `nextjs-16` | -| Building AI chat features | `ai-sdk-5` | -| Changing button labels or section headings on a tour-covered page | `prowler-tour` | -| Committing changes | `prowler-commit` | -| Create PR that requires changelog entry | `prowler-changelog` | -| Creating Zod schemas | `zod-4` | -| Creating a git commit | `prowler-commit` | -| Creating/modifying Prowler UI components | `prowler-ui` | -| Editing a UI file containing data-tour-id attributes | `prowler-tour` | -| Fixing bug | `tdd` | -| Implementing feature | `tdd` | -| Modifying component | `tdd` | -| Refactoring code | `tdd` | -| Renaming or removing a data-tour-id attribute value | `prowler-tour` | -| Restructuring routes or layouts covered by a tour | `prowler-tour` | -| Review changelog format and conventions | `prowler-changelog` | -| Reviewing Prowler UI components | `prowler-ui` | -| Testing hooks or utilities | `vitest` | -| Update CHANGELOG.md in any component | `prowler-changelog` | -| Using Zustand stores | `zustand-5` | -| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` | -| Working on task | `tdd` | -| Working with Prowler UI test helpers/pages | `prowler-test-ui` | -| Working with Tailwind classes | `tailwind-4` | -| Writing Playwright E2E tests | `playwright` | -| Writing Prowler UI E2E tests | `prowler-test-ui` | -| Writing React component tests | `vitest` | -| Writing React components | `react-19` | -| Writing TypeScript types/interfaces | `typescript` | -| Writing Vitest tests | `vitest` | -| Writing unit tests for UI | `vitest` | +| Action | Skill | +| -------------------------------------------------------------- | ------------------- | +| Add changelog entry for a PR or feature | `prowler-changelog` | +| App Router / Server Actions | `nextjs-16` | +| Building AI chat features | `ai-sdk-5` | +| Committing changes | `prowler-commit` | +| Create PR that requires changelog entry | `prowler-changelog` | +| Creating Zod schemas | `zod-4` | +| Creating a git commit | `prowler-commit` | +| Creating/modifying Prowler UI components | `prowler-ui` | +| Fixing bug | `tdd` | +| Implementing feature | `tdd` | +| Modifying component | `tdd` | +| Refactoring code | `tdd` | +| Review changelog format and conventions | `prowler-changelog` | +| Reviewing Prowler UI components | `prowler-ui` | +| Testing hooks or utilities | `vitest` | +| Update CHANGELOG.md in any component | `prowler-changelog` | +| Using Zustand stores | `zustand-5` | +| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` | +| Working on task | `tdd` | +| Working with Prowler UI test helpers/pages | `prowler-test-ui` | +| Working with Tailwind classes | `tailwind-4` | +| Writing Playwright E2E tests | `playwright` | +| Writing Prowler UI E2E tests | `prowler-test-ui` | +| Writing React component tests | `vitest` | +| Writing React components | `react-19` | +| Writing TypeScript types/interfaces | `typescript` | +| Writing Vitest tests | `vitest` | +| Writing unit tests for UI | `vitest` | --- diff --git a/ui/actions/invitations/invitation.adapter.ts b/ui/actions/invitations/invitation.adapter.ts index 41fccefbb8..216cc1cbf4 100644 --- a/ui/actions/invitations/invitation.adapter.ts +++ b/ui/actions/invitations/invitation.adapter.ts @@ -1,4 +1,4 @@ -import type { SentInvitation } from "@/types/onboarding-invite"; +import type { SentInvitation } from "@/types/invitations"; const readString = (value: unknown): string | null => typeof value === "string" && value.length > 0 ? value : null; diff --git a/ui/actions/invitations/invitation.test.ts b/ui/actions/invitations/invitation.test.ts index 4f9635a590..fac16619e3 100644 --- a/ui/actions/invitations/invitation.test.ts +++ b/ui/actions/invitations/invitation.test.ts @@ -59,11 +59,11 @@ describe("sendInvite", () => { it("forwards the invitation source as a query param", async () => { // When - await sendInvite(inviteFormData("onboarding")); + await sendInvite(inviteFormData("provider_connect")); // Then expect(fetchMock).toHaveBeenCalledWith( - "https://api.example.com/api/v1/tenants/invitations?source=onboarding", + "https://api.example.com/api/v1/tenants/invitations?source=provider_connect", expect.objectContaining({ method: "POST" }), ); const body = JSON.parse(fetchMock.mock.calls[0]?.[1]?.body as string); diff --git a/ui/actions/invitations/invitation.ts b/ui/actions/invitations/invitation.ts index 620a6e1a1b..01313ab617 100644 --- a/ui/actions/invitations/invitation.ts +++ b/ui/actions/invitations/invitation.ts @@ -53,7 +53,7 @@ export const sendInvite = async (formData: FormData) => { const role = formData.get("role"); const source = formData.get("source"); const url = new URL(`${apiBaseUrl}/tenants/invitations`); - // Origin of the invitation (e.g. `onboarding`); the API may record it. + // Origin of the invitation (e.g. `provider_connect`); the API may record it. if (typeof source === "string" && source) { url.searchParams.set("source", source); } diff --git a/ui/actions/invitations/roles.ts b/ui/actions/invitations/roles.ts index d90695ef91..16dd7e3dba 100644 --- a/ui/actions/invitations/roles.ts +++ b/ui/actions/invitations/roles.ts @@ -1,7 +1,7 @@ "use server"; import { getRoles } from "@/actions/roles"; -import type { InvitationRoleOption } from "@/types/onboarding-invite"; +import type { InvitationRoleOption } from "@/types/invitations"; const ROLES_PAGE_SIZE = 50; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx index fc2704c1d4..26f88dba3f 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.test.tsx @@ -58,7 +58,6 @@ vi.mock("@/components/shadcn/table", () => ({ data, metadata, controlledPage, - getRowAttributes, }: { columns: Array<{ id?: string; @@ -75,10 +74,6 @@ vi.mock("@/components/shadcn/table", () => ({ }; }; controlledPage: number; - getRowAttributes?: (row: { - index: number; - original: AttackPathScan; - }) => Record; }) => (
{metadata.pagination.count} Total Entries @@ -100,8 +95,8 @@ vi.mock("@/components/shadcn/table", () => ({ - {data.map((row, index) => ( - + {data.map((row) => ( + {columns.map((column, index) => ( {column.cell @@ -181,20 +176,6 @@ describe("ScanListTable", () => { ); }); - it("anchors the attack paths scan tour to the first visible scan row", () => { - render( - , - ); - - const firstRow = screen - .getAllByRole("radio", { - name: "Select scan", - })[0] - .closest("tr"); - - expect(firstRow).toHaveAttribute("data-tour-id", "attack-paths-scan-list"); - }); - it("enables the radio button for a failed scan when graph data is ready", async () => { const user = userEvent.setup(); const failedScan: AttackPathScan = { diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx index 5f894580e0..2e946bbe45 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx @@ -295,9 +295,6 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => { handleSelectScan(row.original.id); } }} - getRowAttributes={(row) => - row.index === 0 ? { "data-tour-id": "attack-paths-scan-list" } : {} - } enableRowSelection rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)} /> diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-attack-path-scans.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-attack-path-scans.ts index 5957e175d7..a010e1f932 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-attack-path-scans.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-attack-path-scans.ts @@ -6,16 +6,6 @@ import { getAttackPathScans } from "@/actions/attack-paths"; import { useMountEffect } from "@/hooks/use-mount-effect"; import type { AttackPathScan } from "@/types/attack-paths"; -export interface UseAttackPathScansOptions { - /** - * Invoked once the initial load resolves with no scan whose graph data is - * ready (including empty results or a fetch failure). The page passes a - * redirect only during onboarding replay; an established user gets `undefined` - * and stays on the page. - */ - onNoReadyScan?: () => void; -} - export interface UseAttackPathScansResult { scans: AttackPathScan[]; scansLoading: boolean; @@ -30,11 +20,7 @@ export interface UseAttackPathScansResult { * project forbids `useEffect` in components, but a reusable data hook is the * sanctioned place for a mount-time fetch when no fetching library is wired up. */ -export function useAttackPathScans( - options: UseAttackPathScansOptions = {}, -): UseAttackPathScansResult { - const { onNoReadyScan } = options; - +export function useAttackPathScans(): UseAttackPathScansResult { const [scans, setScans] = useState([]); const [scansLoading, setScansLoading] = useState(true); const [loadError, setLoadError] = useState(false); @@ -64,20 +50,15 @@ export function useAttackPathScans( if (!mountedRef.current) return; if (scansData?.data) { setScans(scansData.data); - if (!scansData.data.some((scan) => scan.attributes.graph_data_ready)) { - onNoReadyScan?.(); - } } else { setScans([]); setLoadError(true); - onNoReadyScan?.(); } } catch (error) { if (!mountedRef.current) return; console.error("Failed to load scans:", error); setScans([]); setLoadError(true); - onNoReadyScan?.(); } finally { if (mountedRef.current) setScansLoading(false); } diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts index b5aa61db43..389911e0ff 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts @@ -203,13 +203,10 @@ export class AttackPathPageHarness extends BrowserHarness { } get queryBuilderCard(): HTMLElement | null { - return ( - this.container - .querySelector( - '[data-tour-id="attack-paths-query-selector"]', - ) - ?.closest('[data-slot="card"]') ?? null - ); + const executeButton = Array.from( + this.container.querySelectorAll("button"), + ).find((button) => /Execut(e|ing) Query/.test(button.textContent ?? "")); + return executeButton?.closest('[data-slot="card"]') ?? null; } /** Whether the query-parameters panel is shown. */ diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx index 815701868a..58d2dd9c3d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx @@ -1,7 +1,7 @@ "use client"; import { ArrowLeft, Maximize2 } from "lucide-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import { Suspense, useEffect, useRef, useState } from "react"; import { FormProvider } from "react-hook-form"; @@ -15,7 +15,6 @@ import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result import type { GraphHandle } from "@/components/attack-paths/graph/attack-path-graph"; import { FindingDetailDrawer } from "@/components/findings/table"; import { LighthouseContextContributor } from "@/components/lighthouse/context-contributor"; -import { PageReady } from "@/components/onboarding"; import { AutoRefresh } from "@/components/scans"; import { Button, Card, useToast } from "@/components/shadcn"; import { @@ -28,7 +27,6 @@ import { } from "@/components/shadcn/dialog"; import { StatusAlert } from "@/components/shared/status-alert"; import { useFindingDetails } from "@/hooks/use-finding-details"; -import { useMountEffect } from "@/hooks/use-mount-effect"; import { exportGraphAsPNG, isProwlerFindingNode } from "@/lib/attack-paths"; import { buildAttackPathView, @@ -38,14 +36,6 @@ import { } from "@/lib/attack-paths/group-graph"; import { buildAttackPathContext } from "@/lib/lighthouse/context/contributions"; import { isCloud } from "@/lib/shared/env"; -import { attackPathsEmptyTour } from "@/lib/tours/attack-paths-empty.tour"; -import { - attackPathsTour, - type AttackPathsTourTarget, - pickDemoQuery, - pickDemoScan, -} from "@/lib/tours/attack-paths.tour"; -import { advanceActiveTour, useDriverTour } from "@/lib/tours/use-driver-tour"; import type { AttackPathQuery, AttackPathQueryError, @@ -87,22 +77,13 @@ const SCROLL_CONTAINER_CLASS = export default function AttackPathsPage() { const searchParams = useSearchParams(); const pathname = usePathname(); - const router = useRouter(); const scanId = searchParams.get("scanId"); - // Onboarding tours are Cloud-only. - const onboardingEnabled = isCloud(); - const isAttackPathsReplay = - onboardingEnabled && searchParams.get("onboarding") === "attack-paths"; const graphState = useGraphState(); const finding = useFindingDetails(); const { toast } = useToast(); const { scans, scansLoading, loadError, refreshScans, retryLoadScans } = - useAttackPathScans({ - onNoReadyScan: isAttackPathsReplay - ? () => router.push("/scans?onboarding=view-first-scan") - : undefined, - }); + useAttackPathScans(); const [queriesLoading, setQueriesLoading] = useState(true); const [queriesError, setQueriesError] = useState(null); @@ -121,62 +102,6 @@ export default function AttackPathsPage() { const queryBuilder = useQueryBuilder(queries); - const hasReadyScan = scans.some((scan) => scan.attributes.graph_data_ready); - const hasNoScans = scans.length === 0; - - useDriverTour(attackPathsEmptyTour, { - // Gate on !loadError: the empty-scans CTA anchor only renders in the - // NO_SCANS view-state, not in the ERROR state (which also has scans === []). - enabled: onboardingEnabled && !scansLoading && !loadError && hasNoScans, - }); - - const { start: startAttackPathsTour } = useDriverTour( - attackPathsTour, - { - enabled: onboardingEnabled && !scansLoading && hasReadyScan, - autoOpen: !isAttackPathsReplay, - // Page owns tour auto-open; OnboardingSequenceBanner is the sole Continue/Skip control. - // pickDemoScan/pickDemoQuery policy lives in attack-paths.tour.ts. - stepHandlers: { - "scan-list": { - onNext: async ({ waitForStep }) => { - const selected = pickDemoScan(scans); - if (!selected) return; - const params = new URLSearchParams(searchParams.toString()); - params.set("scanId", selected.id); - router.push(`${pathname}?${params.toString()}`); - await waitForStep("query-selector"); - }, - }, - "query-selector": { - onNext: async ({ waitForStep }) => { - const selected = pickDemoQuery(queries); - if (!selected) return; - queryBuilder.handleQueryChange(selected.id); - await waitForStep("execute-button"); - }, - }, - }, - }, - ); - - // Onboarding replay entry: start the tour once and strip the `onboarding` - // param. Invoked from , which mounts only when the - // replay conditions hold — so `useMountEffect` fires it exactly once and the - // old `replayStartedRef` run-once guard is gone. - const startAttackPathsReplay = () => { - startAttackPathsTour(); - - const params = new URLSearchParams(searchParams.toString()); - params.delete("onboarding"); - const query = params.toString(); - window.history.replaceState( - null, - "", - query ? `${pathname}?${query}` : pathname, - ); - }; - useEffect(() => { if (!hasResetRef.current) { hasResetRef.current = true; @@ -268,9 +193,6 @@ export default function AttackPathsPage() { return; } - // The tour's execute step is autoAdvance: the real Execute click moves it forward. - advanceActiveTour(); - graphState.startLoading(); graphState.setError(null); @@ -510,13 +432,6 @@ export default function AttackPathsPage() { onRefresh={refreshScans} /> - {isAttackPathsReplay && !scansLoading && hasReadyScan && ( - - )} - - {/* Enables the navbar replay icon once the initial scan load resolves. */} - {!scansLoading && } - {lighthouseContext && ( )} -
+

Select a scan, build a query, and visualize Attack Paths in your infrastructure. @@ -541,12 +456,7 @@ export default function AttackPathsPage() {

Loading scans...

) : viewState === ATTACK_PATHS_VIEW_STATES.NO_SCANS ? ( - // Keep the empty-scans tour anchor: attackPathsEmptyTour targets - // data-tour-id="attack-paths-empty-scans-cta". The panel's NO_SCANS - // render is the same "No scans available" + Go to Scan Jobs CTA. -
- -
+ ) : viewState !== ATTACK_PATHS_VIEW_STATES.READY ? ( -
- -
+ {queryBuilder.selectedQueryData && ( -
+
); } - -interface AttackPathsReplayTriggerProps { - onReplay: () => void; -} - -// Conditional-mount trigger: the parent renders this only when the replay -// should start. The microtask keeps driver.js/flushSync outside React's -// mount lifecycle while still running before the next browser task. -function AttackPathsReplayTrigger({ onReplay }: AttackPathsReplayTriggerProps) { - useMountEffect(() => { - let cancelled = false; - - queueMicrotask(() => { - if (!cancelled) onReplay(); - }); - - return () => { - cancelled = true; - }; - }); - - return null; -} diff --git a/ui/app/(prowler)/attack-paths/layout.tsx b/ui/app/(prowler)/attack-paths/layout.tsx index 52cb6685e9..a15fe02185 100644 --- a/ui/app/(prowler)/attack-paths/layout.tsx +++ b/ui/app/(prowler)/attack-paths/layout.tsx @@ -8,11 +8,7 @@ export default function AttackPathsLayout({ children: React.ReactNode; }) { return ( - } - onboardingAction={{ flowId: "attack-paths" }} - > + }> {children} ); diff --git a/ui/app/(prowler)/compliance/_components/compliance-page-tabs.tsx b/ui/app/(prowler)/compliance/_components/compliance-page-tabs.tsx index 21b459ec76..c885610962 100644 --- a/ui/app/(prowler)/compliance/_components/compliance-page-tabs.tsx +++ b/ui/app/(prowler)/compliance/_components/compliance-page-tabs.tsx @@ -66,7 +66,7 @@ export const CompliancePageTabs = ({ {/* Wraps below the tabs on narrow viewports instead of squeezing the triggers, which are the primary navigation of the page. */}
-
+
0 - : true; - return ( - } - onboardingAction={ - hasCompletedScan - ? { flowId: "view-compliance" } - : { - flowId: "view-compliance", - fallbackFlowId: "view-first-scan", - useFallback: true, - } - } - > + }> } - onboardingAction={{ - flowId: "view-compliance", - fallbackFlowId: "view-first-scan", - useFallback: true, - }} - > + }> scan.id === selectedScanId, ); @@ -321,11 +284,7 @@ export default async function Compliance({ ); return ( - } - onboardingAction={onboardingAction} - > + }> } - onboardingAction={{ flowId: "explore-findings" }} - > + }>
diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index 4b4930e4be..8f2a11c60a 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -40,7 +40,7 @@ export default async function Findings({ const { filters, query } = extractFiltersAndQuery(resolvedSearchParams); // The page shell awaits only what both the filters and the table depend on: - // the completed scans (onboarding + scan filter) and the scan date range. + // the completed scans (scan filter) and the scan date range. const [scansData, filtersWithScanDates] = await Promise.all([ getScans({ pageSize: 50, @@ -68,21 +68,8 @@ export default async function Findings({ scan.attributes.unique_resource_count > 1, ) || []; - const onboardingAction = - completedScans.length > 0 - ? { flowId: "explore-findings" } - : { - flowId: "explore-findings", - fallbackFlowId: "view-first-scan", - useFallback: true, - }; - return ( - } - onboardingAction={onboardingAction} - > + }>
}> diff --git a/ui/app/(prowler)/layout.tsx b/ui/app/(prowler)/layout.tsx index ebb8a9c53c..7fc8e6ef82 100644 --- a/ui/app/(prowler)/layout.tsx +++ b/ui/app/(prowler)/layout.tsx @@ -5,14 +5,9 @@ import { Metadata, Viewport } from "next"; import { ReactNode, Suspense } from "react"; import { getProviders } from "@/actions/providers"; -import { getScansByState } from "@/actions/scans/scans"; import { auth } from "@/auth.config"; import MainLayout from "@/components/layout/main-layout/main-layout"; -import { - OnboardingCheckpointWatcher, - OnboardingGate, - OnboardingSequenceBanner, -} from "@/components/onboarding"; +import { FirstRunRedirect } from "@/components/providers/first-run-redirect"; import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config"; import { NavigationProgress } from "@/components/shadcn/navigation-progress"; import { Toaster } from "@/components/shadcn/toast"; @@ -26,7 +21,6 @@ import { evaluateRegistryAccess } from "@/lib/registry/access.server"; import { isCloud } from "@/lib/shared/env"; import { cn } from "@/lib/utils"; import { StoreInitializer } from "@/store/ui/store-initializer"; -import { SCAN_STATES } from "@/types/attack-paths"; import { Providers } from "../providers"; @@ -56,37 +50,19 @@ export default async function RootLayout({ }: { children: ReactNode; }) { - // Skip Cloud-only onboarding fetches and orchestrators in OSS. const cloudEnabled = isCloud(); // Every deployment needs the provider count: it drives the first-run redirect // and the sidebar's Add Provider action. const providersPromise = getProviders({ page: 1, pageSize: 1 }); + const session = await auth(); // One-time server-side Registry gate per request: only an ELIGIBLE answer - // shows the sidebar entry; UNKNOWN and INELIGIBLE both hide it. Started - // here so it resolves in parallel with the Cloud onboarding fetches. - const registryAccessPromise = auth().then((session) => - evaluateRegistryAccess(session?.accessToken), - ); - - // Fail-open: unknown scan state is treated as "has data" so the banner never blocks - // progression on a fetch error. - let hasCompletedScan = true; - // Scopes the onboarding steps' local markers, so resolving them for one - // tenant does not silence them for another. - let tenantId: string | null = null; - - if (cloudEnabled) { - const scansByState = await getScansByState(); - hasCompletedScan = Array.isArray(scansByState?.data) - ? scansByState.data.some( - (scan: { attributes?: { state?: string } }) => - scan.attributes?.state === SCAN_STATES.COMPLETED, - ) - : true; - tenantId = (await auth())?.tenantId ?? null; - } + // shows the sidebar entry; UNKNOWN and INELIGIBLE both hide it. + const registryAccessPromise = evaluateRegistryAccess(session?.accessToken); + // Scopes the first-run marker, so resolving it for one tenant does not + // silence it for another. + const tenantId = session?.tenantId ?? null; const providersData = await providersPromise; // Tri-state: true = has providers, false = zero providers, undefined = fetch failed (gate fails open). @@ -119,15 +95,7 @@ export default async function RootLayout({ {/* Tri-state for both: an unknown count leaves the store unresolved and the gate closed. */} {/* Every deployment: an empty tenant lands on the add-provider wizard once. */} - - {cloudEnabled && ( - <> - {/* Single mount point so the watcher survives post-connect navigation. */} - - {/* Persistent banner shown only while a guided sequence is active. */} - - - )} + {children} {cloudEnabled && } {/* Always mounted: it hosts the detail (finding/resource) views in diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx index 03b54ac566..cd277ee922 100644 --- a/ui/app/(prowler)/providers/page.tsx +++ b/ui/app/(prowler)/providers/page.tsx @@ -23,22 +23,12 @@ export default async function Providers({ const activeTab = getProviderTab(resolvedSearchParams.tab); const isCloudEnvironment = isCloud(); - // Exclude `tab` and `onboarding` from the key: tab switches must not re-suspend, - // and `onboarding` is ephemeral (stripped via history.replaceState) — keeping it - // would remount ProvidersAccountsView and reset the wizard mid-flow. - const { - tab: _tab, - onboarding: _onboarding, - ...stableParams - } = resolvedSearchParams || {}; + // Exclude `tab` from the key: tab switches must not re-suspend. + const { tab: _tab, ...stableParams } = resolvedSearchParams || {}; const searchParamsKey = JSON.stringify(stableParams); return ( - } - onboardingAction={{ flowId: "add-provider" }} - > + }> {isCloudEnvironment && } ({ authMock: vi.fn(), @@ -24,7 +23,6 @@ const { getAllProviderGroupsMock: vi.fn(), getScansMock: vi.fn(), redirectMock: vi.fn(), - contentLayoutSpy: vi.fn(), scansPageShellSpy: vi.fn(), })); @@ -53,13 +51,9 @@ vi.mock("@/actions/schedules", () => ({ // The shell is exercised in its own test; here we only assert the page always // mounts it (with a table subtree) and never gates on provider state. vi.mock("@/components/shadcn/content-layout", () => ({ - ContentLayout: (props: { - children: ReactNode; - onboardingAction: unknown; - }) => { - contentLayoutSpy(props); - return
{props.children}
; - }, + ContentLayout: (props: { children: ReactNode }) => ( +
{props.children}
+ ), })); vi.mock("@/components/scans/scans-page-shell", () => ({ ScansPageShell: (props: { @@ -127,51 +121,6 @@ describe("scans page rendering", () => { expect(await screen.findByRole("table")).toBeInTheDocument(); }, ); - - it("uses the plain view-first-scan onboarding action when a provider is connected", async () => { - getAllProvidersMock.mockResolvedValue({ data: [connectedProvider] }); - - await renderPage(); - - const props = contentLayoutSpy.mock.calls.at(-1)?.[0]; - expect(props?.onboardingAction).toEqual({ flowId: "view-first-scan" }); - }); - - it("falls back to add-provider onboarding when a permitted user has no connected provider", async () => { - getAllProvidersMock.mockResolvedValue({ data: [disconnectedProvider] }); - - await renderPage(); - - const props = contentLayoutSpy.mock.calls.at(-1)?.[0]; - expect(props?.onboardingAction).toEqual({ - flowId: "view-first-scan", - fallbackFlowId: "add-provider", - useFallback: true, - }); - }); - - // ScansPageShell only mounts the view-first-scan trigger when Launch Scan is - // usable, so offering the navbar action without manage_scans yields an enabled - // button that can never start a tour. - it.each([ - ["a provider is connected", [connectedProvider]], - ["no provider is connected", [disconnectedProvider]], - ["there are no providers", [] as ProviderProps[]], - ])( - "omits the onboarding action when manage_scans is missing and %s", - async (_state, providers) => { - authMock.mockResolvedValue({ - user: { permissions: { manage_scans: false } }, - }); - getAllProvidersMock.mockResolvedValue({ data: providers }); - - await renderPage(); - - const props = contentLayoutSpy.mock.calls.at(-1)?.[0]; - expect(props).toHaveProperty("onboardingAction"); - expect(props?.onboardingAction).toBeUndefined(); - }, - ); }); describe("scans page scheduled tab source", () => { diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index 9e5aee2144..1b7b259fc7 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -187,11 +187,6 @@ export default async function Scans({ const providers = providersData?.data ?? []; const providerGroups = providerGroupsData?.data ?? []; - const hasConnectedProvider = providers.some( - (provider: ProviderProps) => - provider.attributes.connection.connected === true, - ); - const hasManageScansPermission = Boolean( session?.user?.permissions?.manage_scans, ); @@ -202,26 +197,8 @@ export default async function Scans({ getActiveScanCount(resolvedSearchParams), isReportDownloadLocked(), ]); - // Mirrors ScansPageShell's launch gate: it only mounts the view-first-scan trigger - // when Launch Scan is usable (manage_scans + a connected provider). Without the - // permission nothing can consume the navbar action, so offer none rather than an - // enabled button that starts no tour. - const onboardingAction = !hasManageScansPermission - ? undefined - : hasConnectedProvider - ? { flowId: "view-first-scan" } - : { - flowId: "view-first-scan", - fallbackFlowId: "add-provider", - useFallback: true, - }; - return ( - } - onboardingAction={onboardingAction} - > + }> ({ LighthouseContextContributor: () => null, })); -type ViewComplianceStepHandlers = { - [K in ViewComplianceTourTarget]?: TourStepHandlers; -}; - -// Captured so the tour's own step handlers can be exercised: the trigger is a -// driver.js host, and the handlers are the only part of it this grid owns. -const capturedStepHandlers = vi.hoisted(() => ({ - current: {} as ViewComplianceStepHandlers, -})); - -vi.mock("@/components/onboarding", () => ({ - OnboardingTrigger: ({ - stepHandlers, - }: { - stepHandlers: ViewComplianceStepHandlers; - }) => { - capturedStepHandlers.current = stepHandlers; - return null; - }, - PageReady: () => null, -})); - vi.mock("@/actions/compliance-watchlist", () => ({ addComplianceToWatchlist: vi.fn(), bulkUpdateComplianceWatchlist: vi.fn(), @@ -252,63 +227,3 @@ describe("ComplianceOverviewGrid with a universal framework", () => { ); }); }); - -describe("ComplianceOverviewGrid tour anchor", () => { - const searchHandlers = () => - capturedStepHandlers.current[VIEW_COMPLIANCE_TOUR_TARGETS.SEARCH]; - - it("keeps the anchor on the first rendered card regardless of pin state", () => { - renderGrid({ - catalogEntries: [ - catalogEntry("cis_1.4_aws", false), - catalogEntry("gdpr_aws", true), - catalogEntry("iso27001_aws", false), - ], - providerType: "aws", - }); - - expect( - document.querySelector('[data-tour-id="view-compliance-frameworks"]'), - ).toHaveTextContent("CIS"); - }); - - it("waits for the framework card when one will render", async () => { - const waitForStep = vi - .fn() - .mockResolvedValue(document.createElement("div")); - - renderGrid(); - const onNext = searchHandlers()?.onNext; - if (!onNext) throw new Error("Expected a search step handler"); - await onNext({ waitForStep }); - - expect(waitForStep).toHaveBeenCalledWith("frameworks"); - }); - - it("skips the wait when the persisted filter leaves no card to anchor to", async () => { - // The filter survives reloads, so the tour can start on a grid that renders - // the empty state instead of a card — and waiting for an anchor that never - // mounts would hang it there. - useComplianceWatchlistViewStore.setState({ showOnlyWatchlist: true }); - const waitForStep = vi - .fn() - .mockResolvedValue(document.createElement("div")); - - renderGrid({ - catalogEntries: [ - catalogEntry("cis_1.4_aws", false), - catalogEntry("gdpr_aws", false), - catalogEntry("iso27001_aws", false), - ], - providerType: "aws", - }); - const onNext = searchHandlers()?.onNext; - if (!onNext) throw new Error("Expected a search step handler"); - await onNext({ waitForStep }); - - expect(waitForStep).not.toHaveBeenCalled(); - expect( - document.querySelector('[data-tour-id="view-compliance-frameworks"]'), - ).toBeNull(); - }); -}); diff --git a/ui/components/compliance/compliance-overview-grid.tsx b/ui/components/compliance/compliance-overview-grid.tsx index e28e7566f3..b82c81c876 100644 --- a/ui/components/compliance/compliance-overview-grid.tsx +++ b/ui/components/compliance/compliance-overview-grid.tsx @@ -1,15 +1,13 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Suspense, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useState } from "react"; import { ComplianceCard } from "@/components/compliance/compliance-card"; import { ComplianceFrameworkGrid } from "@/components/compliance/compliance-framework-grid"; import { LighthouseContextContributor } from "@/components/lighthouse/context-contributor"; -import { OnboardingTrigger, PageReady } from "@/components/onboarding"; import { DataTableSearch } from "@/components/shadcn/table/data-table-search"; import { useShowOnlyWatchlist } from "@/hooks/use-show-only-watchlist"; -import { buildComplianceDetailPath } from "@/lib/compliance/compliance-detail-url"; import { buildWatchlistIndex, isFrameworkPinned, @@ -20,8 +18,6 @@ import { LIGHTHOUSE_CONTEXT_CONTRIBUTOR_LIMIT, } from "@/lib/lighthouse/context/constants"; import { buildComplianceContext } from "@/lib/lighthouse/context/contributions"; -import { getFlowById } from "@/lib/onboarding"; -import { createViewComplianceTourStepHandlers } from "@/lib/tours/view-compliance.tour"; import type { ComplianceOverviewData } from "@/types/compliance"; import type { ComplianceCatalogEntry } from "@/types/compliance-watchlist"; import { WATCHLIST_PIN_STATE } from "@/types/compliance-watchlist"; @@ -30,15 +26,6 @@ import type { ScanEntity } from "@/types/scans"; import { WatchlistEmptyState } from "./watchlist/watchlist-empty-state"; import { WatchlistToggle } from "./watchlist/watchlist-toggle"; -const viewComplianceFlow = getFlowById("view-compliance")!; - -// Module-level so the identity is stable: `configOverrides` is an effect dependency in -// `useDriverTour`, and a fresh object per keystroke would tear the tour down mid-typing. -const VIEW_COMPLIANCE_TOUR_CONFIG = { - // Last step opens the first card (see createViewComplianceTourStepHandlers). - doneBtnText: "Open Compliance", -}; - interface ComplianceOverviewGridProps { frameworks: ComplianceOverviewData[]; scanId: string; @@ -60,7 +47,6 @@ export const ComplianceOverviewGrid = ({ canManageWatchlist = false, subscriptionOnly = false, }: ComplianceOverviewGridProps) => { - const router = useRouter(); const searchParams = useSearchParams(); const [searchTerm, setSearchTerm] = useState(""); const showOnlyWatchlist = useShowOnlyWatchlist(); @@ -92,34 +78,6 @@ export const ComplianceOverviewGrid = ({ const visibleFrameworks = filterToWatchlist ? filteredFrameworks.filter((compliance) => isPinned(compliance.id)) : filteredFrameworks; - const tourAnchorId = visibleFrameworks[0]?.id; - - const resetSearch = () => { - setSearchTerm(""); - // Clearing the search does not bring the anchor back while the persisted - // watchlist filter is on and nothing is pinned: the grid renders the empty - // state instead, so the tour has to skip the step rather than wait for a - // selector that never mounts. - return filterToWatchlist ? pinnedTotal > 0 : frameworks.length > 0; - }; - - const openFirstFramework = () => { - // The fallback covers a search that filtered every card away — never the - // watchlist filter, where opening a hidden framework would contradict the - // list the user is looking at. - const first = - visibleFrameworks[0] ?? (filterToWatchlist ? undefined : frameworks[0]); - if (!first) return; - router.push( - buildComplianceDetailPath({ - title: first.attributes.framework, - complianceId: first.id, - version: first.attributes.version, - scanId, - regionFilter: searchParams.get("filter[region__in]"), - }), - ); - }; const renderGrid = (items: ComplianceOverviewData[]) => ( @@ -129,13 +87,7 @@ export const ComplianceOverviewGrid = ({ attributes; return ( -
+
))} - {/* Suspense required: OnboardingTrigger reads useSearchParams */} - - - - {/* Signals the navbar that this route's data has loaded (enables the replay icon). */} -
-
- -
+ {visibleFrameworks.length.toLocaleString()} Total Entries diff --git a/ui/components/findings/findings-filters.tsx b/ui/components/findings/findings-filters.tsx index 51df2e2a3f..919320af9e 100644 --- a/ui/components/findings/findings-filters.tsx +++ b/ui/components/findings/findings-filters.tsx @@ -383,7 +383,7 @@ export const FindingsFilters = (props: FindingsFiltersProps) => { }); return ( -
+
({ DataTable: ({ data, toolbarRightContent, - getRowAttributes, getRowCanSelect, onRowSelectionChange, renderAfterRow, @@ -52,10 +51,6 @@ vi.mock("@/components/shadcn/table", () => ({ }: { data?: Array<{ checkId?: string }>; toolbarRightContent?: ReactNode; - getRowAttributes?: (row: { - index: number; - original: { checkId?: string }; - }) => Record; getRowCanSelect?: (row: { index: number; original: { checkId?: string }; @@ -88,7 +83,6 @@ vi.mock("@/components/shadcn/table", () => ({ data-can-select={String( getRowCanSelect?.({ index, original }) ?? true, )} - {...getRowAttributes?.({ index, original })} > {original.checkId} @@ -136,39 +130,6 @@ vi.mock("@/components/lighthouse/context-contributor", () => ({ ), })); -vi.mock("@/components/onboarding", () => ({ - // Exposes the "Open a finding group" step so tests can drive it and read - // whether the handler waited for the resources step. - OnboardingTrigger: ({ - stepHandlers, - }: { - stepHandlers?: Record< - string, - { - onNext?: (context: { - waitForStep: (target: string) => Promise; - }) => Promise; - } - >; - }) => ( - - ), - PageReady: () =>
, -})); - vi.mock("@/components/filters/custom-checkbox-muted-findings", () => ({ CustomCheckboxMutedFindings: () => (