Add Orbit excluded groups administration
What does this MR do and why?
Adds an admin-only Excluded groups card to Orbit settings. Administrators can add or remove top-level groups from automatic indexing exclusions, including before automatic indexing is enabled. This supersedes the closed, stale merge request !254180 (closed).
References
Related to #606375 (closed) and #629978 (closed)
Screenshots or screen recordings
| Empty state | Exclude modal |
|---|---|
![]() |
![]() |
| Populated list |
|---|
![]() |
How to set up and validate locally
- Enable Orbit and sign in as an administrator.
- Go to
/admin/orbit. - In Excluded groups, select Exclude group.
- Select a top-level group and select Exclude.
- Verify that the group appears in the list and that Remove removes it.
- Go to a group Orbit settings page and verify that Excluded groups is not rendered.
Agent context - implementation and verification
- The query and mutations require instance administrator access and Orbit availability.
- The mutations define admin-only, instance-scoped granular token permissions.
- Duplicate add requests are idempotent, including a database uniqueness race.
- The existing enrollment validation returns a clean base error without an attribute prefix.
- The frontend query uses
@gl_introduced(version: "19.5.0")for rolling-update compatibility. - GraphQL request specs passed with 34 examples, including authorization and duplicate handling.
- Three focused Jest suites passed with 13 tests.
- ESLint, Prettier, RuboCop, GraphQL validation, permissions validation, gettext lint, and docs lint passed.
- GraphQL docs, permission docs, translation strings, schema, and introspection artifacts were regenerated.
Database review
These are local GDK EXPLAIN (ANALYZE, BUFFERS) plans on a tiny dataset, not Database Lab or production-scale plans.
`find_or_initialize_for` lookup
SELECT "knowledge_graph_excluded_namespaces".* FROM "knowledge_graph_excluded_namespaces" WHERE "knowledge_graph_excluded_namespaces"."root_namespace_id" = 105 LIMIT 1;Limit (cost=0.15..2.37 rows=1 width=24) (actual time=0.002..0.002 rows=1 loops=1)
Buffers: shared hit=2
-> Index Scan using knowledge_graph_excluded_namespaces_pkey on knowledge_graph_excluded_namespaces (cost=0.15..2.37 rows=1 width=24) (actual time=0.002..0.002 rows=1 loops=1)
Index Cond: (root_namespace_id = 105)
Buffers: shared hit=2
Planning:
Buffers: shared hit=8
Planning Time: 0.030 ms
Execution Time: 0.005 msThe primary key provides the Index Scan for the single-row lookup.
`namespace_is_not_excluded` existence check
SELECT 1 AS one FROM "knowledge_graph_excluded_namespaces" WHERE "knowledge_graph_excluded_namespaces"."root_namespace_id" = 105 LIMIT 1;Limit (cost=0.15..2.37 rows=1 width=4) (actual time=0.004..0.005 rows=1 loops=1)
Buffers: shared hit=2
-> Index Only Scan using knowledge_graph_excluded_namespaces_pkey on knowledge_graph_excluded_namespaces (cost=0.15..2.37 rows=1 width=4) (actual time=0.004..0.004 rows=1 loops=1)
Index Cond: (root_namespace_id = 105)
Heap Fetches: 1
Buffers: shared hit=2
Planning Time: 0.007 ms
Execution Time: 0.008 msThe primary key provides the Index Only Scan for the existence check.
`remove_for(namespace)` delete
DELETE FROM "knowledge_graph_excluded_namespaces" WHERE "knowledge_graph_excluded_namespaces"."root_namespace_id" = 105;Delete on knowledge_graph_excluded_namespaces (cost=0.15..2.37 rows=0 width=0) (actual time=0.002..0.002 rows=0 loops=1)
Buffers: shared hit=3
-> Index Scan using knowledge_graph_excluded_namespaces_pkey on knowledge_graph_excluded_namespaces (cost=0.15..2.37 rows=1 width=6) (actual time=0.001..0.001 rows=1 loops=1)
Index Cond: (root_namespace_id = 105)
Buffers: shared hit=2
Planning Time: 0.005 ms
Execution Time: 0.004 msThe primary key provides the Index Scan that locates the row to delete.
Excluded namespace create
INSERT INTO "knowledge_graph_excluded_namespaces" ("root_namespace_id", "created_at", "updated_at") VALUES (105, '2026-09-17 15:24:20', '2026-09-17 15:24:20') RETURNING "root_namespace_id";Insert on knowledge_graph_excluded_namespaces (cost=0.00..0.01 rows=1 width=24) (actual time=0.006..0.006 rows=1 loops=1)
Buffers: shared hit=3
-> Result (cost=0.00..0.01 rows=1 width=24) (actual time=0.000..0.000 rows=1 loops=1)
Planning Time: 0.005 ms
Trigger for constraint fk_rails_c36a52edca: time=0.033 calls=1
Execution Time: 0.043 msThe insert uses a Result node and completes the foreign-key trigger check.
Excluded namespaces resolver list
SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id", "namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level", "namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error", "namespaces"."ldap_sync_last_update_at", "namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at", "namespaces"."lfs_enabled", "namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit", "namespaces"."require_two_factor_authentication", "namespaces"."two_factor_grace_period", "namespaces"."project_creation_level", "namespaces"."runners_token", "namespaces"."saml_discovery_token", "namespaces"."runners_token_encrypted", "namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit", "namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level", "namespaces"."subgroup_creation_level", "namespaces"."max_pages_size", "namespaces"."max_artifacts_size", "namespaces"."mentions_disabled", "namespaces"."default_branch_protection", "namespaces"."max_personal_access_token_lifetime", "namespaces"."shared_runners_enabled", "namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids", "namespaces"."organization_id", "namespaces"."state" FROM "namespaces" INNER JOIN "knowledge_graph_excluded_namespaces" ON "knowledge_graph_excluded_namespaces"."root_namespace_id" = "namespaces"."id" WHERE "namespaces"."type" = 'Group' ORDER BY "namespaces"."name" ASC LIMIT 100;Limit (cost=0.30..13.93 rows=100 width=371) (actual time=0.016..0.019 rows=1 loops=1)
Buffers: shared hit=16
-> Nested Loop (cost=0.30..16.38 rows=118 width=371) (actual time=0.016..0.018 rows=1 loops=1)
Buffers: shared hit=16
-> Index Scan using index_namespaces_name_parent_id_type on namespaces (cost=0.14..6.39 rows=7 width=371) (actual time=0.008..0.012 rows=8 loops=1)
Index Cond: ((type)::text = 'Group'::text)
Buffers: shared hit=7
-> Index Only Scan using knowledge_graph_excluded_namespaces_pkey on knowledge_graph_excluded_namespaces (cost=0.15..1.43 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=8)
Index Cond: (root_namespace_id = namespaces.id)
Heap Fetches: 1
Buffers: shared hit=9
Planning:
Buffers: shared hit=14
Planning Time: 0.142 ms
Execution Time: 0.029 msThe resolver plan uses index_namespaces_name_parent_id_type for the ordered Group scan and knowledge_graph_excluded_namespaces_pkey for the join. It is bounded by LIMIT 100. The local planner did not choose a sequential scan on namespaces; on production-sized data, the first index supports ORDER BY namespaces.name while filtering to type = 'Group', and the exclusion primary key provides one lookup per candidate namespace.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
- Exclusion management is available only in instance administrator mode.
- The card remains visible regardless of the automatic indexing toggle.
- Add, remove, empty, populated, authorization, and duplicate cases are covered.
- GraphQL, token permission, and Orbit documentation are updated.


