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:
nameWithNamespacesub-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 flag — ai_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
- In GDK as
root, enable the flag:bundle exec rails runner 'Feature.enable(:ai_catalog_filter_enabled_projects)' - 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). - Navigate to Explore → AI Catalog, open that agent's detail page.
- Click Enable → in the project dropdown,
group-b/consumer-projectnow 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;- Plan, 1 pair (lower bound): https://postgres.ai/console/gitlab/gitlab-production-main/sessions/51209/commands/151534 — index scan, ~3.5 ms
- Plan, 18 pairs (realistic modal load): https://console.postgres.ai/gitlab/gitlab-production-main/sessions/51241/commands/151593 — index scan, ~6.117 ms total (planning + execution)
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, ...);- Plan: https://postgres.ai/console/gitlab/gitlab-production-main/sessions/51209/commands/151536 — primary-key index scan on
ai_catalog_items_pkey, ~4.5 ms
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
- I have evaluated the MR acceptance checklist for this merge request.