diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb index 2522bae08a2ae1fbd6013efd0b66666f286762ff..f9bd4e300cec1df64f350cb00fd9f3fbb749f09d 100644 --- a/app/graphql/types/namespace_type.rb +++ b/app/graphql/types/namespace_type.rb @@ -107,6 +107,13 @@ class NamespaceType < BaseObject extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1) end + field :sidebar, + Types::Namespaces::SidebarType, + null: true, + description: 'Data needed to render the sidebar for the namespace.', + method: :itself, + alpha: { milestone: '17.6' } + markdown_field :description_html, null: true def achievements_path diff --git a/app/graphql/types/namespaces/sidebar_type.rb b/app/graphql/types/namespaces/sidebar_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..b95f469b517593b0214d3b66a54e20fc7b2b2ebc --- /dev/null +++ b/app/graphql/types/namespaces/sidebar_type.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Types + module Namespaces + class SidebarType < BaseObject # rubocop:disable Graphql/AuthorizeTypes -- parent is already authorized + graphql_name 'NamespaceSidebar' + + alias_method :namespace, :object + + field :open_issues_count, + GraphQL::Types::Int, + null: true, + description: 'Number of open issues of the namespace.' + + field :open_merge_requests_count, # rubocop:disable GraphQL/ExtractType -- no need to extract these into a field named "open" + GraphQL::Types::Int, + null: true, + description: 'Number of open merge requests of the namespace.' + + def open_issues_count + case namespace + when ::Group + group_open_issues_count + when ::Namespaces::ProjectNamespace + namespace.project.open_issues_count(context[:current_user]) + end + end + + def open_merge_requests_count + case namespace + when Group + ::Groups::MergeRequestsCountService.new(namespace, context[:current_user]).count + when ::Namespaces::ProjectNamespace + namespace.project.open_merge_requests_count + end + end + + def group_open_issues_count + ::Groups::OpenIssuesCountService.new(namespace, context[:current_user], fast_timeout: true).count + rescue ActiveRecord::QueryCanceled => e # rubocop:disable Database/RescueQueryCanceled -- used with fast_read_statement_timeout to prevent this count from slowing down the rest of the request + Gitlab::ErrorTracking.log_exception(e, group_id: namespace.id, query: 'group_sidebar_issues_count') + + nil + end + end + end +end + +Types::Namespaces::SidebarType.prepend_mod diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1a654d5e83a5f2bea91a62b03c1d4ad19aa3204f..db6e74ee44560d00c6727f621c203bab0fcbac57 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -23979,6 +23979,7 @@ GPG signature for a signed commit. | <a id="groupsecuritypolicyproject"></a>`securityPolicyProject` | [`Project`](#project) | Security policy project assigned to the namespace. | | <a id="groupsharewithgrouplock"></a>`shareWithGroupLock` | [`Boolean`](#boolean) | Indicates if sharing a project with another group within this group is prevented. | | <a id="groupsharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. | +| <a id="groupsidebar"></a>`sidebar` **{warning-solid}** | [`NamespaceSidebar`](#namespacesidebar) | **Introduced** in GitLab 17.6. **Status**: Experiment. Data needed to render the sidebar for the namespace. | | <a id="groupstandardroles"></a>`standardRoles` **{warning-solid}** | [`StandardRoleConnection`](#standardroleconnection) | **Introduced** in GitLab 17.4. **Status**: Experiment. Standard roles available for the instance, available only for self-managed. | | <a id="groupstats"></a>`stats` | [`GroupStats`](#groupstats) | Group statistics. | | <a id="groupstoragesizelimit"></a>`storageSizeLimit` | [`Float`](#float) | The storage limit (in bytes) included with the root namespace plan. This limit only applies to namespaces under namespace limit enforcement. | @@ -28856,6 +28857,7 @@ Product analytics events for a specific month and year. | <a id="namespacerootstoragestatistics"></a>`rootStorageStatistics` | [`RootStorageStatistics`](#rootstoragestatistics) | Aggregated storage statistics of the namespace. Only available for root namespaces. | | <a id="namespacesecuritypolicyproject"></a>`securityPolicyProject` | [`Project`](#project) | Security policy project assigned to the namespace. | | <a id="namespacesharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. | +| <a id="namespacesidebar"></a>`sidebar` **{warning-solid}** | [`NamespaceSidebar`](#namespacesidebar) | **Introduced** in GitLab 17.6. **Status**: Experiment. Data needed to render the sidebar for the namespace. | | <a id="namespacestoragesizelimit"></a>`storageSizeLimit` | [`Float`](#float) | The storage limit (in bytes) included with the root namespace plan. This limit only applies to namespaces under namespace limit enforcement. | | <a id="namespacesubscriptionhistory"></a>`subscriptionHistory` **{warning-solid}** | [`GitlabSubscriptionHistoryConnection`](#gitlabsubscriptionhistoryconnection) | **Introduced** in GitLab 17.3. **Status**: Experiment. Find subscription history records. | | <a id="namespacetimelogcategories"></a>`timelogCategories` **{warning-solid}** | [`TimeTrackingTimelogCategoryConnection`](#timetrackingtimelogcategoryconnection) | **Introduced** in GitLab 15.3. **Status**: Experiment. Timelog categories for the namespace. | @@ -29168,6 +29170,16 @@ four standard [pagination arguments](#pagination-arguments): | <a id="namespacecommitemailnamespace"></a>`namespace` | [`Namespace!`](#namespace) | Namespace. | | <a id="namespacecommitemailupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the namespace commit email was last updated. | +### `NamespaceSidebar` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="namespacesidebaropenepicscount"></a>`openEpicsCount` | [`Int`](#int) | Number of open epics of the namespace. | +| <a id="namespacesidebaropenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues of the namespace. | +| <a id="namespacesidebaropenmergerequestscount"></a>`openMergeRequestsCount` | [`Int`](#int) | Number of open merge requests of the namespace. | + ### `NestedEnvironment` Describes where code is deployed for a project organized by folder. diff --git a/ee/app/graphql/ee/types/namespaces/sidebar_type.rb b/ee/app/graphql/ee/types/namespaces/sidebar_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..db0f0a9ce45a3776dc0e7e168295b2c838bca33a --- /dev/null +++ b/ee/app/graphql/ee/types/namespaces/sidebar_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module EE + module Types + module Namespaces + module SidebarType + extend ActiveSupport::Concern + + prepended do + field :open_epics_count, + GraphQL::Types::Int, + null: true, + description: 'Number of open epics of the namespace.' + end + + def open_epics_count + return unless namespace.is_a?(Group) + + ::Groups::EpicsCountService.new(namespace, context[:current_user]).count + end + end + end + end +end diff --git a/ee/spec/graphql/ee/types/namespaces/sidebar_type_spec.rb b/ee/spec/graphql/ee/types/namespaces/sidebar_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f76b5de222b76bb714cf8cef11eaf7feaedf21d1 --- /dev/null +++ b/ee/spec/graphql/ee/types/namespaces/sidebar_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['NamespaceSidebar'], feature_category: :navigation do + let(:fields) do + %i[open_issues_count open_merge_requests_count open_epics_count] + end + + specify { expect(described_class).to have_graphql_fields(fields) } +end diff --git a/ee/spec/requests/api/graphql/namespaces/sidebar_spec.rb b/ee/spec/requests/api/graphql/namespaces/sidebar_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..94ced4c342eafe0c896d5de886073e77a7733375 --- /dev/null +++ b/ee/spec/requests/api/graphql/namespaces/sidebar_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Namespace.sidebar', feature_category: :navigation do + include GraphqlHelpers + + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, :repository, namespace: group) } + + let_it_be(:reporter) { create(:user, reporter_of: group) } + + let(:query) do + <<~QUERY + query { + namespace(fullPath: "#{namespace.full_path}") { + sidebar { + openEpicsCount + } + } + } + QUERY + end + + before_all do + create_list(:epic, 2, group: group) + end + + before do + stub_licensed_features(epics: true) + end + + context 'with a Group' do + let(:namespace) { group } + + it 'returns the epic counts' do + post_graphql(query, current_user: reporter) + + expect(response).to have_gitlab_http_status(:ok) + + expect(graphql_data_at(:namespace, :sidebar)).to eq({ + 'openEpicsCount' => 2 + }) + end + end + + context 'with a ProjectNamespace' do + let(:namespace) { project.project_namespace } + + it 'returns nil epic count' do + post_graphql(query, current_user: reporter) + + expect(response).to have_gitlab_http_status(:ok) + + expect(graphql_data_at(:namespace, :sidebar)).to eq({ + 'openEpicsCount' => nil + }) + end + end +end diff --git a/lib/sidebars/groups/menus/issues_menu.rb b/lib/sidebars/groups/menus/issues_menu.rb index 76a3e759df35c13cc4b85af910314409c83e80c9..417f59af633d3d537da5540bc3542b4ba8524be8 100644 --- a/lib/sidebars/groups/menus/issues_menu.rb +++ b/lib/sidebars/groups/menus/issues_menu.rb @@ -44,6 +44,8 @@ def pill_count end rescue ActiveRecord::QueryCanceled => e # rubocop:disable Database/RescueQueryCanceled -- used with fast_read_statement_timeout to prevent counts from slowing down the request Gitlab::ErrorTracking.log_exception(e, group_id: context.group.id, query: 'group_sidebar_issues_count') + + nil end override :pill_html_options diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb index 331b87b02178f1a5908d43a4e0e7080bee8d9d35..0357117f6d0cb437905c51298ec166da6daf2105 100644 --- a/spec/graphql/types/namespace_type_spec.rb +++ b/spec/graphql/types/namespace_type_spec.rb @@ -12,6 +12,7 @@ id name path full_name full_path achievements_path description description_html visibility lfs_enabled request_access_enabled projects root_storage_statistics shared_runners_setting timelog_categories achievements work_item pages_deployments import_source_users work_item_types + sidebar ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/namespaces/sidebar_type_spec.rb b/spec/graphql/types/namespaces/sidebar_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..62d27c456b1e11719c48e6a847b8f954d8d42fbf --- /dev/null +++ b/spec/graphql/types/namespaces/sidebar_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['NamespaceSidebar'], feature_category: :navigation do + let(:fields) do + %i[open_issues_count open_merge_requests_count] + end + + specify { expect(described_class.graphql_name).to eq('NamespaceSidebar') } + + specify { expect(described_class).to have_graphql_fields(fields).at_least } +end diff --git a/spec/lib/sidebars/groups/menus/issues_menu_spec.rb b/spec/lib/sidebars/groups/menus/issues_menu_spec.rb index cac0cc1edc5047277e329c65c2ed05426c7bbe2c..74e10e9dea20dd33782fd2b311b645f6a5b4c985 100644 --- a/spec/lib/sidebars/groups/menus/issues_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/issues_menu_spec.rb @@ -64,7 +64,7 @@ it 'logs the error and returns a null count' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( ActiveRecord::QueryCanceled, group_id: group.id, query: 'group_sidebar_issues_count' - ) + ).and_call_original expect(menu.pill_count).to be_nil end diff --git a/spec/requests/api/graphql/namespaces/sidebar_spec.rb b/spec/requests/api/graphql/namespaces/sidebar_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1fcab4e687378bee37763e05b2334556a9e50018 --- /dev/null +++ b/spec/requests/api/graphql/namespaces/sidebar_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Namespace.sidebar', feature_category: :navigation do + include GraphqlHelpers + + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, :repository, namespace: group) } + + let_it_be(:reporter) { create(:user, reporter_of: group) } + + let(:query) do + <<~QUERY + query { + namespace(fullPath: "#{namespace.full_path}") { + sidebar { + openIssuesCount + openMergeRequestsCount + } + } + } + QUERY + end + + before_all do + create_list(:issue, 2, project: project) + create(:merge_request, source_project: project) + end + + context 'with a Group' do + let(:namespace) { group } + + it 'returns the group counts' do + post_graphql(query, current_user: reporter) + + expect(response).to have_gitlab_http_status(:ok) + + expect(graphql_data_at(:namespace, :sidebar)).to eq({ + 'openIssuesCount' => 2, + 'openMergeRequestsCount' => 1 + }) + end + + context 'when issue count query times out' do + before do + allow_next_instance_of(::Groups::OpenIssuesCountService) do |service| + allow(service).to receive(:count).and_raise(ActiveRecord::QueryCanceled) + end + end + + it 'logs the error and returns a null issue count' do + expect(Gitlab::ErrorTracking).to receive(:log_exception).with( + ActiveRecord::QueryCanceled, group_id: namespace.id, query: 'group_sidebar_issues_count' + ).and_call_original + + post_graphql(query, current_user: reporter) + + expect(response).to have_gitlab_http_status(:ok) + + expect(graphql_data_at(:namespace, :sidebar)).to eq({ + 'openIssuesCount' => nil, + 'openMergeRequestsCount' => 1 + }) + end + end + end + + context 'with a ProjectNamespace' do + let(:namespace) { project.project_namespace } + + it 'returns the project counts' do + post_graphql(query, current_user: reporter) + + expect(response).to have_gitlab_http_status(:ok) + + expect(graphql_data_at(:namespace, :sidebar)).to eq({ + 'openIssuesCount' => 2, + 'openMergeRequestsCount' => 1 + }) + end + end +end