Commit ac436da7 authored by Jan Provaznik's avatar Jan Provaznik Committed by Igor Drozdov

Add requirement counts to GraphqQL

Expose requirement counts on project type in GraphQL
parent 3b030e56
......@@ -6141,6 +6141,11 @@ type Project {
state: RequirementState
): Requirement
"""
Number of requirements for the project by their state
"""
requirementStatesCount: RequirementStatesCount
"""
Find requirements. Available only when feature flag `requirements_management` is enabled.
"""
......@@ -6923,6 +6928,21 @@ enum RequirementState {
OPENED
}
"""
Counts of requirements by their state.
"""
type RequirementStatesCount {
"""
Number of archived requirements
"""
archived: Int
"""
Number of opened requirements
"""
opened: Int
}
type RootStorageStatistics {
"""
The CI artifacts size in bytes
......
......@@ -18381,6 +18381,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "requirementStatesCount",
"description": "Number of requirements for the project by their state",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "RequirementStatesCount",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "requirements",
"description": "Find requirements. Available only when feature flag `requirements_management` is enabled.",
......@@ -20794,6 +20808,47 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RequirementStatesCount",
"description": "Counts of requirements by their state.",
"fields": [
{
"name": "archived",
"description": "Number of archived requirements",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "opened",
"description": "Number of opened requirements",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RootStorageStatistics",
......
......@@ -901,6 +901,7 @@ Information about pagination in a connection.
| `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
| `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. |
| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
| `serviceDeskAddress` | String | E-mail address of the service desk. |
......@@ -1023,6 +1024,15 @@ Check permissions for the current user on a requirement
| `readRequirement` | Boolean! | Indicates the user can perform `read_requirement` on this resource |
| `updateRequirement` | Boolean! | Indicates the user can perform `update_requirement` on this resource |
## RequirementStatesCount
Counts of requirements by their state.
| Name | Type | Description |
| --- | ---- | ---------- |
| `archived` | Int | Number of archived requirements |
| `opened` | Int | Number of opened requirements |
## RootStorageStatistics
| Name | Type | Description |
......
......@@ -27,6 +27,18 @@ module EE
description: 'Find requirements. Available only when feature flag `requirements_management` is enabled.',
max_page_size: 2000,
resolver: ::Resolvers::RequirementsResolver
field :requirement_states_count, ::Types::RequirementStatesCountType, null: true,
description: 'Number of requirements for the project by their state',
resolve: -> (project, args, ctx) do
return unless requirements_available?(project, ctx[:current_user])
Hash.new(0).merge(project.requirements.counts_by_state)
end
def self.requirements_available?(project, user)
::Feature.enabled?(:requirements_management, project) && Ability.allowed?(user, :read_requirement, project)
end
end
end
end
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class RequirementStatesCountType < BaseObject
graphql_name 'RequirementStatesCount'
description 'Counts of requirements by their state.'
field :opened, GraphQL::INT_TYPE, null: true, description: 'Number of opened requirements'
field :archived, GraphQL::INT_TYPE, null: true, description: 'Number of archived requirements'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -24,6 +24,7 @@ class Requirement < ApplicationRecord
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_state, -> (state) { where(state: state) }
scope :counts_by_state, -> { group(:state).count }
def self.simple_sorts
super.except('name_asc', 'name_desc')
......
......@@ -4,7 +4,10 @@ require 'spec_helper'
describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do
expected_fields = %w[service_desk_enabled service_desk_address vulnerabilities]
expected_fields = %w[
service_desk_enabled service_desk_address vulnerabilities
requirement_states_count
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['RequirementStatesCount'] do
it { expect(described_class.graphql_name).to eq('RequirementStatesCount') }
it 'has specific fields' do
%i[opened archived].each do |field_name|
expect(described_class).to have_graphql_field(field_name)
end
end
end
......@@ -23,4 +23,15 @@ describe Requirement do
it { is_expected.to validate_length_of(:title).is_at_most(::Issuable::TITLE_LENGTH_MAX) }
it { is_expected.to validate_length_of(:title_html).is_at_most(::Issuable::TITLE_HTML_LENGTH_MAX) }
end
describe 'scopes' do
describe '.counts_by_state' do
let_it_be(:opened) { create(:requirement, project: project, state: :opened) }
let_it_be(:archived) { create(:requirement, project: project, state: :archived) }
subject { described_class.counts_by_state }
it { is_expected.to contain_exactly(['archived', 1], ['opened', 1]) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'getting requirement counts for a project' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:requirement1) { create(:requirement, project: project, state: :opened) }
let_it_be(:requirement2) { create(:requirement, project: project, state: :archived) }
let(:counts) { graphql_data['project']['requirementStatesCount'] }
let(:fields) do
<<~QUERY
opened
archived
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('requirementStatesCount', {}, fields)
)
end
shared_examples 'nil requirement counts' do
it 'returns nil' do
post_graphql(query, current_user: current_user)
expect(counts).to be_nil
end
end
context 'when user has access to the project' do
before do
stub_licensed_features(requirements: true)
project.add_developer(current_user)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
it 'returns requirement counts' do
post_graphql(query, current_user: current_user)
expect(graphql_errors).to be_nil
expect(counts['opened']).to eq 1
expect(counts['archived']).to eq 1
end
context 'when requirements_management feature is disabled' do
before do
stub_feature_flags(requirements_management: false)
end
it_behaves_like 'nil requirement counts'
end
end
context 'when the user does not have access to the requirement' do
before do
stub_licensed_features(requirements: true)
end
it 'returns nil' do
post_graphql(query)
expect(graphql_data['project']).to be_nil
end
end
context 'when requirements feature is not available' do
before do
stub_licensed_features(requirements: false)
project.add_developer(current_user)
end
it_behaves_like 'nil requirement counts'
end
context 'when there are no requirements in the project' do
let(:project) { create(:project) }
before do
stub_licensed_features(requirements: true)
project.add_developer(current_user)
end
it 'returns zero values for missing states' do
post_graphql(query, current_user: current_user)
expect(graphql_errors).to be_nil
expect(counts['opened']).to eq 0
expect(counts['archived']).to eq 0
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment