Commit aa3ea161 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '229046-iterations-charts-graphql-endpoint' into 'master'

Expose iteration burnup chart time series through GraphQL

See merge request gitlab-org/gitlab!41666
parents 0ca47d0b 44e21f40
...@@ -8555,7 +8555,12 @@ enum IssueType { ...@@ -8555,7 +8555,12 @@ enum IssueType {
""" """
Represents an iteration object. Represents an iteration object.
""" """
type Iteration { type Iteration implements TimeboxBurnupTimeSeriesInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
""" """
Timestamp of iteration creation Timestamp of iteration creation
""" """
...@@ -10398,7 +10403,7 @@ type MetricsDashboardAnnotationEdge { ...@@ -10398,7 +10403,7 @@ type MetricsDashboardAnnotationEdge {
""" """
Represents a milestone. Represents a milestone.
""" """
type Milestone { type Milestone implements TimeboxBurnupTimeSeriesInterface {
""" """
Daily scope and completed totals for burnup charts Daily scope and completed totals for burnup charts
""" """
...@@ -16514,6 +16519,13 @@ Time represented in ISO 8601 ...@@ -16514,6 +16519,13 @@ Time represented in ISO 8601
""" """
scalar Time scalar Time
interface TimeboxBurnupTimeSeriesInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
}
type Timelog { type Timelog {
""" """
Timestamp of when the time tracked was spent at. Deprecated in 12.10: Use `spentAt` Timestamp of when the time tracked was spent at. Deprecated in 12.10: Use `spentAt`
......
...@@ -23567,6 +23567,28 @@ ...@@ -23567,6 +23567,28 @@
"name": "Iteration", "name": "Iteration",
"description": "Represents an iteration object.", "description": "Represents an iteration object.",
"fields": [ "fields": [
{
"name": "burnupTimeSeries",
"description": "Daily scope and completed totals for burnup charts",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "createdAt", "name": "createdAt",
"description": "Timestamp of iteration creation", "description": "Timestamp of iteration creation",
...@@ -23798,7 +23820,11 @@ ...@@ -23798,7 +23820,11 @@
], ],
"inputFields": null, "inputFields": null,
"interfaces": [ "interfaces": [
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"ofType": null
}
], ],
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
...@@ -29142,7 +29168,11 @@ ...@@ -29142,7 +29168,11 @@
], ],
"inputFields": null, "inputFields": null,
"interfaces": [ "interfaces": [
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"ofType": null
}
], ],
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
...@@ -48418,6 +48448,50 @@ ...@@ -48418,6 +48448,50 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"description": null,
"fields": [
{
"name": "burnupTimeSeries",
"description": "Daily scope and completed totals for burnup charts",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "Iteration",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Milestone",
"ofType": null
}
]
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Timelog", "name": "Timelog",
...@@ -1295,6 +1295,7 @@ Represents an iteration object. ...@@ -1295,6 +1295,7 @@ Represents an iteration object.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
| `createdAt` | Time! | Timestamp of iteration creation | | `createdAt` | Time! | Timestamp of iteration creation |
| `description` | String | Description of the iteration | | `description` | String | Description of the iteration |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
......
...@@ -6,10 +6,7 @@ module EE ...@@ -6,10 +6,7 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
field :burnup_time_series, [::Types::BurnupChartDailyTotalsType], null: true, implements ::Types::TimeboxBurnupTimeSeriesInterface
resolver: ::Resolvers::MilestoneBurnupTimeSeriesResolver,
description: 'Daily scope and completed totals for burnup charts',
complexity: 175
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module Resolvers module Resolvers
class MilestoneBurnupTimeSeriesResolver < BaseResolver class TimeboxBurnupTimeSeriesResolver < BaseResolver
type [Types::BurnupChartDailyTotalsType], null: true type [Types::BurnupChartDailyTotalsType], null: true
alias_method :milestone, :synchronized_object alias_method :timebox, :synchronized_object
def resolve(*args) def resolve(*args)
return [] unless milestone.burnup_charts_available? return [] unless timebox.burnup_charts_available?
response = TimeboxBurnupChartService.new(milestone).execute response = TimeboxBurnupChartService.new(timebox).execute
raise GraphQL::ExecutionError, response.message if response.error? raise GraphQL::ExecutionError, response.message if response.error?
......
...@@ -9,6 +9,8 @@ module Types ...@@ -9,6 +9,8 @@ module Types
authorize :read_iteration authorize :read_iteration
implements ::Types::TimeboxBurnupTimeSeriesInterface
field :id, GraphQL::ID_TYPE, null: false, field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the iteration' description: 'ID of the iteration'
......
# frozen_string_literal: true
module Types
module TimeboxBurnupTimeSeriesInterface
include BaseInterface
field :burnup_time_series, [::Types::BurnupChartDailyTotalsType], null: true,
resolver: ::Resolvers::TimeboxBurnupTimeSeriesResolver,
description: 'Daily scope and completed totals for burnup charts',
complexity: 175
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::MilestoneBurnupTimeSeriesResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project, start_date: '2020-01-01', due_date: '2020-01-15') }
let_it_be(:issues) { create_list(:issue, 2, project: project) }
before_all do
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-10')
end
before do
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
subject { resolve(described_class, obj: milestone) }
context 'when the feature flag is disabled' do
before do
stub_feature_flags(burnup_charts: false)
end
it 'returns empty data' do
expect(subject).to be_empty
end
end
context 'when the feature flag is enabled' do
before do
stub_feature_flags(burnup_charts: true)
end
it 'returns burnup chart data' do
expect(subject).to eq([
{
date: Date.parse('2020-01-05'),
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-10'),
scope_count: 2,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
}
])
end
context 'when the service returns an error' do
before do
stub_const('TimeboxBurnupChartService::EVENT_COUNT_LIMIT', 1)
end
it 'raises a GraphQL exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError, 'Burnup chart could not be generated due to too many events')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::TimeboxBurnupTimeSeriesResolver do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issues) { create_list(:issue, 2, project: project) }
let_it_be(:start_date) { Date.today }
let_it_be(:due_date) { start_date + 2.weeks }
before do
stub_licensed_features(milestone_charts: true, issue_weights: true, iterations: true)
end
RSpec.shared_examples 'timebox time series' do
subject { resolve(described_class, obj: timebox) }
context 'when the feature flag is disabled' do
before do
stub_feature_flags(burnup_charts: false, iteration_charts: false)
end
it 'returns empty data' do
expect(subject).to be_empty
end
end
context 'when the feature flag is enabled' do
before do
stub_feature_flags(burnup_charts: true, iteration_charts: true)
end
it 'returns burnup chart data' do
expect(subject).to eq([
{
date: start_date + 4.days,
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
},
{
date: start_date + 9.days,
scope_count: 2,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
}
])
end
context 'when the service returns an error' do
before do
stub_const('TimeboxBurnupChartService::EVENT_COUNT_LIMIT', 1)
end
it 'raises a GraphQL exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError, 'Burnup chart could not be generated due to too many events')
end
end
end
end
context 'when timebox is a milestone' do
let_it_be(:timebox) { create(:milestone, project: project, start_date: start_date, due_date: due_date) }
before_all do
create(:resource_milestone_event, issue: issues[0], milestone: timebox, action: :add, created_at: start_date + 4.days)
create(:resource_milestone_event, issue: issues[1], milestone: timebox, action: :add, created_at: start_date + 9.days)
end
it_behaves_like 'timebox time series'
end
context 'when timebox is an iteration' do
let_it_be(:timebox) { create(:iteration, group: group, start_date: start_date, due_date: due_date) }
before_all do
create(:resource_iteration_event, issue: issues[0], iteration: timebox, action: :add, created_at: start_date + 4.days)
create(:resource_iteration_event, issue: issues[1], iteration: timebox, action: :add, created_at: start_date + 9.days)
end
it_behaves_like 'timebox time series'
end
end
...@@ -6,4 +6,13 @@ RSpec.describe GitlabSchema.types['Iteration'] do ...@@ -6,4 +6,13 @@ RSpec.describe GitlabSchema.types['Iteration'] do
it { expect(described_class.graphql_name).to eq('Iteration') } it { expect(described_class.graphql_name).to eq('Iteration') }
it { expect(described_class).to require_graphql_authorizations(:read_iteration) } it { expect(described_class).to require_graphql_authorizations(:read_iteration) }
it 'has the expected fields' do
expected_fields = %w[
id id title description state web_path web_url scoped_path scoped_url
due_date start_date created_at updated_at burnup_time_series
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
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