Commit 9845e480 authored by Nathan Friend's avatar Nathan Friend Committed by Igor Drozdov

Add milestone stats to GraphQL endpoint

This commit updates the milestone GraphQL type to allow statistics about
the milestone to be queried.
parent 358d35e0
# 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