diff --git a/app/graphql/types/project_feature_access_level_enum.rb b/app/graphql/types/project_feature_access_level_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..a107dbedc52fb1fe22ee557a8a3b5d90e8c8a09f --- /dev/null +++ b/app/graphql/types/project_feature_access_level_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class ProjectFeatureAccessLevelEnum < BaseEnum + graphql_name 'ProjectFeatureAccessLevel' + description 'Access level of a project feature' + + value 'DISABLED', value: ProjectFeature::DISABLED, description: 'Not enabled for anyone.' + value 'PRIVATE', value: ProjectFeature::PRIVATE, description: 'Enabled only for team members.' + value 'ENABLED', value: ProjectFeature::ENABLED, description: 'Enabled for everyone able to access the project.' + end +end diff --git a/app/graphql/types/project_feature_access_level_type.rb b/app/graphql/types/project_feature_access_level_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..d6d9fdefa70fe39068340bb4933d199cf2c4a380 --- /dev/null +++ b/app/graphql/types/project_feature_access_level_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# rubocop:disable Graphql/AuthorizeTypes -- It just returns the value of an enum as an integer and a string +module Types + class ProjectFeatureAccessLevelType < Types::BaseObject + graphql_name 'ProjectFeatureAccess' + description 'Represents the access level required by the user to access a project feature' + + field :integer_value, GraphQL::Types::Int, null: true, + description: 'Integer representation of access level.', + method: :to_i + + field :string_value, Types::ProjectFeatureAccessLevelEnum, null: true, + description: 'String representation of access level.', + method: :to_i + end +end +# rubocop:enable Graphql/AuthorizeTypes diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index ec87f1338439ceb79c687d9b65bcc8b3d26ab48f..97db338ad1c4b64a1cb214f7b7c59a216c3f8257 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -151,6 +151,10 @@ class ProjectType < BaseObject null: true, description: 'Number of open issues for the project.' + field :open_merge_requests_count, GraphQL::Types::Int, + null: true, + description: 'Number of open merge requests for the project.' + field :allow_merge_on_skipped_pipeline, GraphQL::Types::Boolean, null: true, description: 'If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of ' \ @@ -675,6 +679,19 @@ def label(title:) end end + [:issues, :forking, :merge_requests].each do |feature| + field_name = "#{feature}_access_level" + feature_name = feature.to_s.tr("_", " ") + + field field_name, Types::ProjectFeatureAccessLevelType, + null: true, + description: "Access level required for #{feature_name} access." + + define_method field_name do + project.project_feature&.access_level(feature) + end + end + markdown_field :description_html, null: true def avatar_url @@ -689,6 +706,12 @@ def open_issues_count BatchLoader::GraphQL.wrap(object.open_issues_count) if object.feature_available?(:issues, context[:current_user]) end + def open_merge_requests_count + return unless object.feature_available?(:merge_requests, context[:current_user]) + + BatchLoader::GraphQL.wrap(object.open_merge_requests_count) + end + def forks_count BatchLoader::GraphQL.wrap(object.forks_count) end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1137e67bb42b1ec2832ed62f74b141642ba2018a..d5e80e0549f6ed65bcdb0c7ee400ab3b0c37d43b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -23819,6 +23819,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectdetailedimportstatus"></a>`detailedImportStatus` | [`DetailedImportStatus`](#detailedimportstatus) | Detailed import status of the project. | | <a id="projectdora"></a>`dora` | [`Dora`](#dora) | Project's DORA metrics. | | <a id="projectflowmetrics"></a>`flowMetrics` **{warning-solid}** | [`ProjectValueStreamAnalyticsFlowMetrics`](#projectvaluestreamanalyticsflowmetrics) | **Introduced** in 15.10. This feature is an Experiment. It can be changed or removed at any time. Flow metrics for value stream analytics. | +| <a id="projectforkingaccesslevel"></a>`forkingAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for forking access. | | <a id="projectforkscount"></a>`forksCount` | [`Int!`](#int) | Number of times the project has been forked. | | <a id="projectfullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the project. | | <a id="projectgrafanaintegration"></a>`grafanaIntegration` | [`GrafanaIntegration`](#grafanaintegration) | Grafana integration details for the project. | @@ -23829,6 +23830,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectimportstatus"></a>`importStatus` | [`String`](#string) | Status of import background job of the project. | | <a id="projectincidentmanagementtimelineeventtags"></a>`incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. | | <a id="projectiscatalogresource"></a>`isCatalogResource` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Indicates if a project is a catalog resource. | +| <a id="projectissuesaccesslevel"></a>`issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. | | <a id="projectissuesenabled"></a>`issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user. | | <a id="projectjiraimportstatus"></a>`jiraImportStatus` | [`String`](#string) | Status of Jira import background job of the project. | | <a id="projectjiraimports"></a>`jiraImports` | [`JiraImportConnection`](#jiraimportconnection) | Jira imports into the project. (see [Connections](#connections)) | @@ -23837,6 +23839,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectlastactivityat"></a>`lastActivityAt` | [`Time`](#time) | Timestamp of the project last activity. | | <a id="projectlfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if the project has Large File Storage (LFS) enabled. | | <a id="projectmergecommittemplate"></a>`mergeCommitTemplate` | [`String`](#string) | Template used to create merge commit message in merge requests. | +| <a id="projectmergerequestsaccesslevel"></a>`mergeRequestsAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for merge requests access. | | <a id="projectmergerequestsdisablecommittersapproval"></a>`mergeRequestsDisableCommittersApproval` | [`Boolean!`](#boolean) | Indicates that committers of the given merge request cannot approve. | | <a id="projectmergerequestsenabled"></a>`mergeRequestsEnabled` | [`Boolean`](#boolean) | Indicates if Merge Requests are enabled for the current user. | | <a id="projectmergerequestsffonlyenabled"></a>`mergeRequestsFfOnlyEnabled` | [`Boolean`](#boolean) | Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. | @@ -23847,6 +23850,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectonlyallowmergeifallstatuscheckspassed"></a>`onlyAllowMergeIfAllStatusChecksPassed` | [`Boolean`](#boolean) | Indicates that merges of merge requests should be blocked unless all status checks have passed. | | <a id="projectonlyallowmergeifpipelinesucceeds"></a>`onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. | | <a id="projectopenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues for the project. | +| <a id="projectopenmergerequestscount"></a>`openMergeRequestsCount` | [`Int`](#int) | Number of open merge requests for the project. | | <a id="projectpackagescleanuppolicy"></a>`packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy for the project. | | <a id="projectpackagesprotectionrules"></a>`packagesProtectionRules` | [`PackagesProtectionRuleConnection`](#packagesprotectionruleconnection) | Packages protection rules for the project. (see [Connections](#connections)) | | <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. | @@ -25373,6 +25377,17 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectdatatransferegressnodes"></a>`egressNodes` | [`EgressNodeConnection`](#egressnodeconnection) | Data nodes. (see [Connections](#connections)) | | <a id="projectdatatransfertotalegress"></a>`totalEgress` | [`BigInt`](#bigint) | Total egress for that project in that period of time. | +### `ProjectFeatureAccess` + +Represents the access level required by the user to access a project feature. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectfeatureaccessintegervalue"></a>`integerValue` | [`Int`](#int) | Integer representation of access level. | +| <a id="projectfeatureaccessstringvalue"></a>`stringValue` | [`ProjectFeatureAccessLevel`](#projectfeatureaccesslevel) | String representation of access level. | + ### `ProjectMember` Represents a Project Membership. @@ -30740,6 +30755,16 @@ Current state of the product analytics stack. | <a id="productanalyticsstateloading_instance"></a>`LOADING_INSTANCE` | Stack is currently initializing. | | <a id="productanalyticsstatewaiting_for_events"></a>`WAITING_FOR_EVENTS` | Stack is waiting for events from users. | +### `ProjectFeatureAccessLevel` + +Access level of a project feature. + +| Value | Description | +| ----- | ----------- | +| <a id="projectfeatureaccessleveldisabled"></a>`DISABLED` | Not enabled for anyone. | +| <a id="projectfeatureaccesslevelenabled"></a>`ENABLED` | Enabled for everyone able to access the project. | +| <a id="projectfeatureaccesslevelprivate"></a>`PRIVATE` | Enabled only for team members. | + ### `ProjectMemberRelation` Project member relation. diff --git a/spec/graphql/types/project_feature_access_level_enum_spec.rb b/spec/graphql/types/project_feature_access_level_enum_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a13b3be3f8ff45bece90fad466bb2b4c2f6fc65e --- /dev/null +++ b/spec/graphql/types/project_feature_access_level_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ProjectFeatureAccessLevel'], feature_category: :groups_and_projects do + specify { expect(described_class.graphql_name).to eq('ProjectFeatureAccessLevel') } + + it 'exposes all the existing access levels' do + expect(described_class.values.keys).to include(*%w[DISABLED PRIVATE ENABLED]) + end +end diff --git a/spec/graphql/types/project_feature_access_level_type_spec.rb b/spec/graphql/types/project_feature_access_level_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fae9de63d93b70d25585245f727a9434394eb355 --- /dev/null +++ b/spec/graphql/types/project_feature_access_level_type_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ProjectFeatureAccess'], feature_category: :groups_and_projects do + specify { expect(described_class.graphql_name).to eq('ProjectFeatureAccess') } + specify { expect(described_class).to require_graphql_authorizations(nil) } + + it 'has expected fields' do + expected_fields = [:integer_value, :string_value] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 7b4bcf4b1b0e83b55bebb469b34e4f7261c048f9..fc3dd28c297520e479db47ac6194a1dbd9272494 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -5,6 +5,7 @@ RSpec.describe GitlabSchema.types['Project'] do include GraphqlHelpers include ProjectForksHelper + using RSpec::Parameterized::TableSyntax specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) } @@ -21,7 +22,8 @@ container_registry_enabled shared_runners_enabled lfs_enabled merge_requests_ff_only_enabled avatar_url issues_enabled merge_requests_enabled wiki_enabled - snippets_enabled jobs_enabled public_jobs open_issues_count import_status + forking_access_level issues_access_level merge_requests_access_level + snippets_enabled jobs_enabled public_jobs open_issues_count open_merge_requests_count import_status only_allow_merge_if_pipeline_succeeds request_access_enabled only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled namespace group statistics statistics_details_paths repository merge_requests merge_request issues @@ -704,6 +706,63 @@ end end + describe 'project features access level' do + let_it_be(:project) { create(:project, :public) } + + where(project_feature: %w[forkingAccessLevel issuesAccessLevel mergeRequestsAccessLevel]) + + with_them do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + #{project_feature} { + integerValue + stringValue + } + } + } + ) + end + + subject { GitlabSchema.execute(query).as_json.dig('data', 'project', project_feature) } + + it { is_expected.to eq({ "integerValue" => ProjectFeature::ENABLED, "stringValue" => "ENABLED" }) } + end + end + + describe 'open_merge_requests_count' do + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:open_merge_request) { create(:merge_request, source_project: project) } + let_it_be(:closed_merge_request) { create(:merge_request, :closed, source_project: project) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + openMergeRequestsCount + } + } + ) + end + + subject(:open_merge_requests_count) do + GitlabSchema.execute(query).as_json.dig('data', 'project', 'openMergeRequestsCount') + end + + context 'when the user can access merge requests' do + it { is_expected.to eq(1) } + end + + context 'when the user cannot access merge requests' do + before do + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it { is_expected.to be_nil } + end + end + describe 'branch_rules' do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public) }