Filter already-enabled projects in AI Catalog Enable modal

What does this MR do and why?

Addresses the second problem from #596801 (closed): when enabling a public agent/flow from the Explore catalog, the project picker currently lists every project the user can maintain — including projects where the item is already enabled. Picking one of those and submitting produced a confusing "already enabled" error.

After this MR, rows for projects where the item is already enabled are rendered disabled with an "Already enabled" badge, so the user can't pick them:

  • nameWithNamespace sub-label preserved for every row (helpful when several of your projects have similar names)
  • Right-aligned neutral <gl-badge> reading "Already enabled" on the affected rows
  • Disabled rows can't be clicked or keyboard-selected

How this works

Backend — new GraphQL field aiCatalogItemConsumer(itemId:) on Project that surfaces the per-project consumer record. Batched with BatchLoader so the consumer lookup is a single query regardless of how many projects are returned. Authorization flows through the existing read_ai_catalog_item_consumer ability on the type.

Frontend — new ai_catalog_available_projects.query.graphql requests the existing project list fields plus aiCatalogItemConsumer { id, enabled } per row. The modal passes item.id down so FormProjectDropdown picks the new query. The shared SingleSelectDropdown got two small additive, optional props (itemDisabledFn, itemTrailingLabelFn) so it can now render a per-row disabled state and a trailing badge without changing its existing callers.

Feature flagai_catalog_filter_enabled_projects, default off. When off, the modal passes null for item-id and the dropdown falls back to the existing getProjects query. This gives us a clean zero-downtime story across the rolling deploy window and acts as a kill-switch after rollout.

How to reproduce

  1. In GDK as root, enable the flag:
    bundle exec rails runner 'Feature.enable(:ai_catalog_filter_enabled_projects)'
  2. Create a public agent in some project (e.g. group-a/owning-project) and enable it in one of your other maintainer projects (e.g. group-b/consumer-project).
  3. Navigate to Explore → AI Catalog, open that agent's detail page.
  4. Click Enable → in the project dropdown, group-b/consumer-project now renders greyed-out with an "Already enabled" badge and can't be selected. Other maintainer projects remain selectable.

Before this MR (or with the flag off): all rows are selectable, and picking group-b/consumer-project fails on submit with "already enabled".

Screenshots / recordings

walkthrough master branch
walkthrough feature branch

Database review

This MR introduces no migrations. It adds two new read queries against existing tables (ai_catalog_item_consumers, ai_catalog_items) via a new GraphQL field Project.aiCatalogItemConsumer(itemId:). Queries are batched across all projects in a single GraphQL request via BatchLoader, and the consumer's project association is pre-assigned to the already-loaded record so the type-level authorization pass doesn't re-fetch it.

New queries

Q1 — consumer batch lookup (fired once per GraphQL request, regardless of project count)

SQL captured from development.log
SELECT "ai_catalog_item_consumers".*
FROM   "ai_catalog_item_consumers"
WHERE  ("ai_catalog_item_consumers"."project_id" = $1
        AND "ai_catalog_item_consumers"."ai_catalog_item_id" = $2
        OR "ai_catalog_item_consumers"."project_id" = $3
        AND "ai_catalog_item_consumers"."ai_catalog_item_id" = $4
        OR ... -- one (project_id, ai_catalog_item_id) pair per project in the modal
       )
ORDER  BY "ai_catalog_item_consumers"."id" ASC
LIMIT  1000;

Q2 — items eager-load (fired once per GraphQL request, via .with_items / includes(:item))

SQL captured from development.log
SELECT "ai_catalog_items".*
FROM   "ai_catalog_items"
WHERE  "ai_catalog_items"."id" IN ($1, $2, ...);

Indexes used

ai_catalog_item_consumers has single-column indexes on project_id (index_ai_catalog_item_consumers_on_project_id) and ai_catalog_item_id, with no composite index on the pair. The planner uses the project_id index for each disjunct in the OR-chain and filters by ai_catalog_item_id afterwards. At realistic modal scale (≤ ~50 projects per request) this stays sub‑millisecond on production data; an explicit composite index can be added later if data grows.

ai_catalog_items lookups go straight to the primary key.

MR acceptance checklist

Edited by Keeyan Nejad

Merge request reports

Loading