Frontend dropdown for security policy eligible projects

Summary

Updates the policy scope "specific projects" dropdown to use the new securityPolicyEligibleProjects resolver, replacing the multi-query approach that only showed descendant projects.

Part 2 of 2 for #593858

Stacking note: This MR contains all changes (backend + frontend) so it can be tested end-to-end independently. After !231293 (merged) is merged and this branch is rebased, only frontend changes will remain.

Changes

  • New GraphQL query: get_spp_eligible_projects.query.graphql — single query using securityPolicyEligibleProjects with cursor-based pagination
  • Refactored dropdown: LinkedGroupsProjectsDropdown — replaces multi-query approach (fetch linked groups → fetch projects per group) with a single query. Removes client-side aggregation/deduplication. Uses proper cursor-based pagination instead of hardcoded first: 20
  • Updated selector: ScopeProjectSelector — passes SPP fullPath to the refactored dropdown
  • Removed: get_spp_linked_groups_projects.query.graphql (dead code after refactor)
  • Tests: Frontend specs for dropdown and selector

Demo

Description UI
Before
After

What this fixes

Before After
Only descendant projects visible All projects under root ancestors of linked groups
Hardcoded first: 20 limit Proper cursor-based pagination
N+1-like client round-trips (1 query per linked group) Single server-side query
Client-side deduplication needed Server handles deduplication

MR Dependencies

MR1 (Backend) !231293 ──sequential──▶ This MR (Frontend)

How to manually test

This walks through the exact hierarchy from the issue's root-cause example so you can directly observe the projects that were hidden by the bug.

1. Set up the namespace hierarchy

In your GDK, create this structure:

topgroup/
├── subgroup-a/                    ← we will link the SPP to this subgroup
│   ├── project-1                  (project inside the linked subgroup)
│   └── child-group/
│       └── project-2              (descendant of the linked subgroup)
├── subgroup-b/                    ← sibling subgroup, NOT linked to the SPP
│   └── project-3                  (sibling-of-linked-group project)
├── project-4                      (parent-level project, directly under topgroup)
└── security-policies-project      ← will be assigned as the SPP

Step-by-step:

  1. Create top group: topgroup
  2. Create subgroups: topgroup/subgroup-a and topgroup/subgroup-b
  3. Create nested subgroup: topgroup/subgroup-a/child-group
  4. Create the projects above. The exact contents don't matter — only the paths.
  1. Navigate to topgroup/subgroup-aSecure → Policies.
  2. Click Edit policy project and link topgroup/security-policies-project.

Only subgroup-a is linked. Neither topgroup nor subgroup-b has an SPP assigned. This is the configuration that exposes the bug.

3. Open the policy editor

  1. On topgroup/subgroup-aSecure → Policies, click New policy.
  2. Pick any policy type (Scan execution / Merge request approval / Pipeline execution / Vulnerability management — they all share the dropdown).
  3. Fill in required fields (name, rule, action) — content doesn't matter.
  4. Scroll to Policy scope → switch to specific projects (or "all projects in this group" with the exception type set to "except specific projects").
  5. Open the projects dropdown.

4. Compare what you see in the dropdown

Project Before (bug) After (this MR)
project-1 (inside linked subgroup-a) visible visible
project-2 (in child-group, descendant of linked subgroup) visible visible
project-3 (in sibling subgroup-b, not linked to SPP) hidden visible
project-4 (parent level, directly under topgroup) hidden visible
security-policies-project (the SPP itself) hidden visible

Additional improvements you should observe in the dropdown:

  • Pagination: if more than 20 eligible projects exist, scrolling loads more pages. (Before: hard-capped at 20 regardless of how many were eligible.)
  • Search: typing in the search box filters server-side across all eligible projects — not just the first 20.
  • Footer count: the footer reflects the count of loaded projects.

5. Verify it works across all policy types

Repeat step 3 for each remaining policy type (Merge request approval, Pipeline execution, Vulnerability management). The dropdown should behave identically — the fix lives in the shared LinkedGroupsProjectsDropdown.

6. GraphQL verification (optional)

In /-/graphql-explorer, run:

query {
  project(fullPath: "topgroup/security-policies-project") {
    securityPolicyEligibleProjects(search: "") {
      nodes { id name fullPath }
      pageInfo { hasNextPage endCursor }
    }
  }
}

Expected nodes: project-1, project-2, project-3, project-4, and security-policies-project. These are all non-archived projects under the root ancestor (topgroup) of the SPP's linked groups that the current user has access to.

Test plan

  • Projects from sibling groups are visible in the dropdown
  • Projects from parent groups are visible in the dropdown
  • Backward compatibility: existing project selection still works
  • Pagination works correctly (no hardcoded first: 20 limit)
  • Search works via server-side filtering
  • Empty state when no eligible projects exist
  • Works for all policy types (Scan Execution, Merge Request Approval, Pipeline Execution, Vulnerability Management)

🤖 Generated with Claude Code

Closes #593858

Edited by Artur Fedorov

Merge request reports

Loading