Show all top-level groups in Orbit admin settings

What does this MR do and why?

On self-managed and Dedicated, the Admin Orbit page shows only the top-level groups that the admin is a member of, and only the first 25. Admins are often not members of most groups, so they cannot see or turn on indexing for them. This MR makes the page list every top-level group, with pagination and search.

How it works

1. The problem: the page asks for member groups

The admin page reused the dashboard query, which sets allAvailable: false (member_namespaces.query.graphql):

query getMemberNamespaces($first: Int = 25) {
  groups(topLevelOnly: true, allAvailable: false, first: $first, sort: "name_asc") {

With allAvailable: false, GroupsFinder returns only the groups of the user. The "admin sees all groups" path needs allAvailable: true (groups_finder.rb):

return [Group.all] if can_read_all_groups? && all_available?

The backend already lets an admin turn indexing on or off for any top-level group. The admin role inherits update_knowledge_graph_setting from owner. Only the list was wrong.

2. A query for the admin page

A new query asks for all top-level groups, 20 at a time (admin_namespaces.query.graphql):

groups(
  topLevelOnly: true
  allAvailable: true
  withKnowledgeGraphEnabled: $withKnowledgeGraphEnabled
  search: $search
  first: 20
  after: $after

The dashboard keeps its member query. Its scope is correct for user data.

3. Two lists from one query

The page runs the query twice (orbit_settings_page.vue):

enabledNamespaces: adminNamespacesSmartQuery({
  variables: { withKnowledgeGraphEnabled: true },
}),
availableNamespaces: adminNamespacesSmartQuery({
  variables() {
    return { search: this.searchTerm || null };
  },
}),
  • Indexed groups shows only enrolled groups, with their status cards.
  • Available groups shows all top-level groups and has a search box. A row that is already indexed shows an "Indexed" badge instead of a button.

Both lists share one load-more method for the Load more button (orbit_settings_page.vue).

References

  • Admin role grants: admin.yml, owner.yml
  • The orbit { indexingStatus itemCounts } group field from 9a5ee3ee also works for an admin who is not a member. A new request spec covers it.

Screenshots or screen recordings

The user is orbit-admin, an admin who is not a member of any group. The instance has 114 top-level groups, and 111 of them are indexed.

Before After
orbit-before orbit-after-indexed
More screenshots: available groups, search, turn on
Available groups Search for "twit" After "Turn on indexing"
orbit-after-available orbit-after-search orbit-after-enabled

How to set up and validate locally

  1. Use a self-managed GDK with an Ultimate license and the Orbit stack running. Set GITLAB_SIMULATE_SAAS to 0.
  2. Create an admin user who is not a member of any group.
  3. Sign in as that user and go to Admin > Orbit.
  4. Check that Indexed groups lists the indexed top-level groups, and that Load more adds the next 20.
  5. In Available groups, search for a group that is not indexed and select Turn on indexing.

Testing

The test ran on gitlab-labcode-vm-2 with the full Orbit stack (schema v100, 271,950 definitions).

Check master This MR
Indexed groups that orbit-admin sees 0 of 111 111 of 111, 20 per page
Available groups that orbit-admin sees 0 of 114 114, with search
Turn on and turn off a group that orbit-admin is not a member of Not possible Works; the database row is added and removed
orbit { indexingStatus itemCounts } for that group Works Works (READY, 15 of 15 projects)

The new request spec runs the real frontend query file as an admin who is not a member. With the old allAvailable: false, both examples fail with expected [] to include "group1". With this MR, they pass.

More test results
  • Jest: orbit_settings_page_spec.js passes 10 of 10.
  • RSpec: admin_namespaces_query_spec.rb and namespace_spec.rb pass 55 examples with 0 failures. The 2 pending examples are skipped by the shared example.
  • Lint: ESLint, Prettier, RuboCop, gettext and Danger pass in lefthook.
  • Pagination: Load more went 20, 40, 60, 80, 100, then 112 groups, and then the button went away.

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.

Agent context: decisions, cost, and limits

Per-row cost. Each page loads 20 groups. knowledgeGraphEnabled is preloaded. For an admin, knowledgeGraphAvailable checks only the license and a feature flag, so it makes no extra query per row.

Why a new query file. Reusing excludable_namespaces.query.graphql would add the knowledgeGraphAvailable policy check to every row of the exclude modal, and that query has no pageInfo. Adding the field to the dashboard queries would add the same cost to every dashboard load.

Why indexed rows stay in Available groups. The GraphQL API has no "not indexed" filter. If the page hid indexed rows in the browser, pages with auto-index on would be almost empty. A badge keeps every page full.

Known limit. After a toggle, both lists refetch, and only the first page comes back. Groups that were loaded with Load more must be loaded again.

Not in scope.

  • The turn-on button does not know if a group is excluded. The mutation returns the existing "excluded from indexing" error.
  • Item counts cover only the admin's organization. Self-managed and Dedicated normally have one.

Diff size. The MR adds 331 lines and removes 60. About 165 added lines are tests. The rest is the second list, pagination and search.

Merge request reports

Loading
Loading