Commit e90fbc2b authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'nfriend-add-issue-stats-to-milestones-graphql' into 'master'

Add milestone stats to GraphQL endpoint

See merge request gitlab-org/gitlab!35066
parents 2f06dde1 9845e480
# frozen_string_literal: true
module Types
class MilestoneStatsType < BaseObject
graphql_name 'MilestoneStats'
description 'Contains statistics about a milestone'
authorize :read_milestone
field :total_issues_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of issues associated with the milestone'
field :closed_issues_count, GraphQL::INT_TYPE, null: true,
description: 'Number of closed issues associated with the milestone'
end
end
...@@ -9,6 +9,8 @@ module Types ...@@ -9,6 +9,8 @@ module Types
authorize :read_milestone authorize :read_milestone
alias_method :milestone, :object
field :id, GraphQL::ID_TYPE, null: false, field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the milestone' description: 'ID of the milestone'
...@@ -47,5 +49,14 @@ module Types ...@@ -47,5 +49,14 @@ module Types
field :subgroup_milestone, GraphQL::BOOLEAN_TYPE, null: false, field :subgroup_milestone, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates if milestone is at subgroup level', description: 'Indicates if milestone is at subgroup level',
method: :subgroup_milestone? method: :subgroup_milestone?
field :stats, Types::MilestoneStatsType, null: true,
description: 'Milestone statistics'
def stats
return unless Feature.enabled?(:graphql_milestone_stats, milestone.project || milestone.group, default_enabled: true)
milestone
end
end end
end end
---
title: Add milestone stats to GraphQL endpoint
merge_request: 35066
author:
type: added
...@@ -7676,6 +7676,11 @@ type Milestone { ...@@ -7676,6 +7676,11 @@ type Milestone {
""" """
state: MilestoneStateEnum! state: MilestoneStateEnum!
"""
Milestone statistics
"""
stats: MilestoneStats
""" """
Indicates if milestone is at subgroup level Indicates if milestone is at subgroup level
""" """
...@@ -7737,6 +7742,21 @@ enum MilestoneStateEnum { ...@@ -7737,6 +7742,21 @@ enum MilestoneStateEnum {
closed closed
} }
"""
Contains statistics about a milestone
"""
type MilestoneStats {
"""
Number of closed issues associated with the milestone
"""
closedIssuesCount: Int
"""
Total number of issues associated with the milestone
"""
totalIssuesCount: Int
}
""" """
The position to which the adjacent object should be moved The position to which the adjacent object should be moved
""" """
......
...@@ -21487,6 +21487,20 @@ ...@@ -21487,6 +21487,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "stats",
"description": "Milestone statistics",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "MilestoneStats",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "subgroupMilestone", "name": "subgroupMilestone",
"description": "Indicates if milestone is at subgroup level", "description": "Indicates if milestone is at subgroup level",
...@@ -21702,6 +21716,47 @@ ...@@ -21702,6 +21716,47 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "MilestoneStats",
"description": "Contains statistics about a milestone",
"fields": [
{
"name": "closedIssuesCount",
"description": "Number of closed issues associated with the milestone",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "totalIssuesCount",
"description": "Total number of issues associated with the milestone",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "ENUM", "kind": "ENUM",
"name": "MoveType", "name": "MoveType",
...@@ -1180,11 +1180,21 @@ Represents a milestone. ...@@ -1180,11 +1180,21 @@ Represents a milestone.
| `projectMilestone` | Boolean! | Indicates if milestone is at project level | | `projectMilestone` | Boolean! | Indicates if milestone is at project level |
| `startDate` | Time | Timestamp of the milestone start date | | `startDate` | Time | Timestamp of the milestone start date |
| `state` | MilestoneStateEnum! | State of the milestone | | `state` | MilestoneStateEnum! | State of the milestone |
| `stats` | MilestoneStats | Milestone statistics |
| `subgroupMilestone` | Boolean! | Indicates if milestone is at subgroup level | | `subgroupMilestone` | Boolean! | Indicates if milestone is at subgroup level |
| `title` | String! | Title of the milestone | | `title` | String! | Title of the milestone |
| `updatedAt` | Time! | Timestamp of last milestone update | | `updatedAt` | Time! | Timestamp of last milestone update |
| `webPath` | String! | Web path of the milestone | | `webPath` | String! | Web path of the milestone |
## MilestoneStats
Contains statistics about a milestone
| Name | Type | Description |
| --- | ---- | ---------- |
| `closedIssuesCount` | Int | Number of closed issues associated with the milestone |
| `totalIssuesCount` | Int | Total number of issues associated with the milestone |
## Namespace ## Namespace
| Name | Type | Description | | Name | Type | Description |
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['MilestoneStats'] do
it { expect(described_class).to require_graphql_authorizations(:read_milestone) }
it 'has the expected fields' do
expected_fields = %w[
total_issues_count closed_issues_count
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
...@@ -6,4 +6,21 @@ RSpec.describe GitlabSchema.types['Milestone'] do ...@@ -6,4 +6,21 @@ RSpec.describe GitlabSchema.types['Milestone'] do
specify { expect(described_class.graphql_name).to eq('Milestone') } specify { expect(described_class.graphql_name).to eq('Milestone') }
specify { expect(described_class).to require_graphql_authorizations(:read_milestone) } specify { expect(described_class).to require_graphql_authorizations(:read_milestone) }
it 'has the expected fields' do
expected_fields = %w[
id title description state web_path
due_date start_date created_at updated_at
project_milestone group_milestone subgroup_milestone
stats
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe 'stats field' do
subject { described_class.fields['stats'] }
it { is_expected.to have_graphql_type(Types::MilestoneStatsType) }
end
end end
...@@ -7,16 +7,17 @@ RSpec.describe 'Milestones through GroupQuery' do ...@@ -7,16 +7,17 @@ RSpec.describe 'Milestones through GroupQuery' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:now) { Time.now } let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group) }
let_it_be(:milestone_1) { create(:milestone, group: group) }
let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) }
let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) }
let_it_be(:milestone_4) { create(:milestone, group: group, state: :closed, start_date: now - 2.days, due_date: now - 1.day) }
let_it_be(:milestone_from_other_group) { create(:milestone, group: create(:group)) }
let(:milestone_data) { graphql_data['group']['milestones']['edges'] }
describe 'Get list of milestones from a group' do describe 'Get list of milestones from a group' do
let_it_be(:group) { create(:group) }
let_it_be(:milestone_1) { create(:milestone, group: group) }
let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) }
let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) }
let_it_be(:milestone_4) { create(:milestone, group: group, state: :closed, start_date: now - 2.days, due_date: now - 1.day) }
let_it_be(:milestone_from_other_group) { create(:milestone, group: create(:group)) }
let(:milestone_data) { graphql_data['group']['milestones']['edges'] }
context 'when the request is correct' do context 'when the request is correct' do
before do before do
fetch_milestones(user) fetch_milestones(user)
...@@ -120,4 +121,89 @@ RSpec.describe 'Milestones through GroupQuery' do ...@@ -120,4 +121,89 @@ RSpec.describe 'Milestones through GroupQuery' do
node_array(milestone_data, extract_attribute) node_array(milestone_data, extract_attribute)
end end
end end
describe 'ensures each field returns the correct value' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:milestone) { create(:milestone, group: group, start_date: now, due_date: now + 1.day) }
let_it_be(:open_issue) { create(:issue, project: project, milestone: milestone) }
let_it_be(:closed_issue) { create(:issue, :closed, project: project, milestone: milestone) }
let(:milestone_query) do
%{
id
title
description
state
webPath
dueDate
startDate
createdAt
updatedAt
projectMilestone
groupMilestone
subgroupMilestone
}
end
def post_query
full_query = graphql_query_for("group",
{ full_path: group.full_path },
[query_graphql_field("milestones", nil, "nodes { #{milestone_query} }")]
)
post_graphql(full_query, current_user: user)
graphql_data.dig('group', 'milestones', 'nodes', 0)
end
it 'returns correct values for scalar fields' do
expect(post_query).to eq({
'id' => global_id_of(milestone),
'title' => milestone.title,
'description' => milestone.description,
'state' => 'active',
'webPath' => milestone_path(milestone),
'dueDate' => milestone.due_date.iso8601,
'startDate' => milestone.start_date.iso8601,
'createdAt' => milestone.created_at.iso8601,
'updatedAt' => milestone.updated_at.iso8601,
'projectMilestone' => false,
'groupMilestone' => true,
'subgroupMilestone' => false
})
end
context 'milestone statistics' do
let(:milestone_query) do
%{
stats {
totalIssuesCount
closedIssuesCount
}
}
end
it 'returns the correct milestone statistics' do
expect(post_query).to eq({
'stats' => {
'totalIssuesCount' => 2,
'closedIssuesCount' => 1
}
})
end
context 'when the graphql_milestone_stats feature flag is disabled' do
before do
stub_feature_flags(graphql_milestone_stats: false)
end
it 'returns nil for the stats field' do
expect(post_query).to eq({
'stats' => nil
})
end
end
end
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