Commit 9022a33d authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'milestone-burnup-charts-graphql' into 'master'

Add milestone burnup charts to GraphQL

See merge request gitlab-org/gitlab!38672
parents 9c95852d 66143fd6
...@@ -60,3 +60,5 @@ module Types ...@@ -60,3 +60,5 @@ module Types
end end
end end
end end
Types::MilestoneType.prepend_if_ee('::EE::Types::MilestoneType')
...@@ -1427,6 +1427,36 @@ type Branch { ...@@ -1427,6 +1427,36 @@ type Branch {
name: String! name: String!
} }
"""
Represents the total number of issues and their weights for a particular day.
"""
type BurnupChartDailyTotals {
"""
Number of closed issues as of this day
"""
completedCount: Int!
"""
Total weight of closed issues as of this day
"""
completedWeight: Int!
"""
Date for burnup totals
"""
date: ISO8601Date!
"""
Number of issues as of this day
"""
scopeCount: Int!
"""
Total weight of issues as of this day
"""
scopeWeight: Int!
}
type CiGroup { type CiGroup {
""" """
Jobs in group Jobs in group
...@@ -9549,6 +9579,11 @@ type MetricsDashboardAnnotationEdge { ...@@ -9549,6 +9579,11 @@ type MetricsDashboardAnnotationEdge {
Represents a milestone. Represents a milestone.
""" """
type Milestone { type Milestone {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
""" """
Timestamp of milestone creation Timestamp of milestone creation
""" """
......
...@@ -3851,6 +3851,109 @@ ...@@ -3851,6 +3851,109 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"description": "Represents the total number of issues and their weights for a particular day.",
"fields": [
{
"name": "completedCount",
"description": "Number of closed issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "completedWeight",
"description": "Total weight of closed issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "date",
"description": "Date for burnup totals",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopeCount",
"description": "Number of issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopeWeight",
"description": "Total weight of issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "CiGroup", "name": "CiGroup",
...@@ -26709,6 +26812,28 @@ ...@@ -26709,6 +26812,28 @@
"name": "Milestone", "name": "Milestone",
"description": "Represents a milestone.", "description": "Represents a milestone.",
"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 milestone creation", "description": "Timestamp of milestone creation",
...@@ -245,6 +245,18 @@ Autogenerated return type of BoardListUpdateLimitMetrics ...@@ -245,6 +245,18 @@ Autogenerated return type of BoardListUpdateLimitMetrics
| `commit` | Commit | Commit for the branch | | `commit` | Commit | Commit for the branch |
| `name` | String! | Name of the branch | | `name` | String! | Name of the branch |
## BurnupChartDailyTotals
Represents the total number of issues and their weights for a particular day.
| Name | Type | Description |
| --- | ---- | ---------- |
| `completedCount` | Int! | Number of closed issues as of this day |
| `completedWeight` | Int! | Total weight of closed issues as of this day |
| `date` | ISO8601Date! | Date for burnup totals |
| `scopeCount` | Int! | Number of issues as of this day |
| `scopeWeight` | Int! | Total weight of issues as of this day |
## CiGroup ## CiGroup
| Name | Type | Description | | Name | Type | Description |
...@@ -1477,6 +1489,7 @@ Represents a milestone. ...@@ -1477,6 +1489,7 @@ Represents a milestone.
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
| `createdAt` | Time! | Timestamp of milestone creation | | `createdAt` | Time! | Timestamp of milestone creation |
| `description` | String | Description of the milestone | | `description` | String | Description of the milestone |
| `dueDate` | Time | Timestamp of the milestone due date | | `dueDate` | Time | Timestamp of the milestone due date |
......
# frozen_string_literal: true
module EE
module Types
module MilestoneType
extend ActiveSupport::Concern
prepended do
field :burnup_time_series, [::Types::BurnupChartDailyTotalsType], null: true,
resolver: ::Resolvers::MilestoneBurnupTimeSeriesResolver,
description: 'Daily scope and completed totals for burnup charts',
complexity: 175
end
end
end
end
# frozen_string_literal: true
module Resolvers
class MilestoneBurnupTimeSeriesResolver < BaseResolver
type [Types::BurnupChartDailyTotalsType], null: true
alias_method :milestone, :synchronized_object
def resolve(*args)
return [] unless milestone.burnup_charts_available?
response = Milestones::BurnupChartService.new(milestone).execute
raise GraphQL::ExecutionError, response.message if response.error?
response.payload
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class BurnupChartDailyTotalsType < BaseObject
graphql_name 'BurnupChartDailyTotals'
description 'Represents the total number of issues and their weights for a particular day.'
field :date, GraphQL::Types::ISO8601Date, null: false,
description: 'Date for burnup totals'
field :scope_count, GraphQL::INT_TYPE, null: false,
description: 'Number of issues as of this day'
field :scope_weight, GraphQL::INT_TYPE, null: false,
description: 'Total weight of issues as of this day'
field :completed_count, GraphQL::INT_TYPE, null: false,
description: 'Number of closed issues as of this day'
field :completed_weight, GraphQL::INT_TYPE, null: false,
description: 'Total weight of closed issues as of this day'
end
end
...@@ -20,6 +20,10 @@ module EE ...@@ -20,6 +20,10 @@ module EE
resource_parent&.feature_available?(feature_name) && supports_weight? resource_parent&.feature_available?(feature_name) && supports_weight?
end end
def supports_burnup_charts?
resource_parent&.feature_available?(:milestone_charts) && supports_weight?
end
def burnup_charts_available? def burnup_charts_available?
::Feature.enabled?(:burnup_charts, resource_parent) ::Feature.enabled?(:burnup_charts, resource_parent)
end end
......
...@@ -28,6 +28,7 @@ class License < ApplicationRecord ...@@ -28,6 +28,7 @@ class License < ApplicationRecord
ldap_group_sync ldap_group_sync
member_lock member_lock
merge_request_approvers merge_request_approvers
milestone_charts
multiple_issue_assignees multiple_issue_assignees
multiple_ldap_servers multiple_ldap_servers
multiple_merge_request_assignees multiple_merge_request_assignees
......
...@@ -12,20 +12,18 @@ class Milestones::BurnupChartService ...@@ -12,20 +12,18 @@ class Milestones::BurnupChartService
EVENT_COUNT_LIMIT = 50_000 EVENT_COUNT_LIMIT = 50_000
TooManyEventsError = Class.new(StandardError)
def initialize(milestone) def initialize(milestone)
raise ArgumentError, 'Milestone must have a start and due date' if milestone.start_date.blank? || milestone.due_date.blank?
@milestone = milestone @milestone = milestone
end end
def execute def execute
return ServiceResponse.error(message: _('Milestone does not support burnup charts')) unless milestone.supports_burnup_charts?
return ServiceResponse.error(message: _('Milestone must have a start and due date')) if milestone.start_date.blank? || milestone.due_date.blank?
return ServiceResponse.error(message: _('Burnup chart could not be generated due to too many events')) if resource_events.num_tuples > EVENT_COUNT_LIMIT
@issue_states = {} @issue_states = {}
@chart_data = [] @chart_data = []
raise TooManyEventsError if resource_events.num_tuples > EVENT_COUNT_LIMIT
resource_events.each do |event| resource_events.each do |event|
case event['event_type'] case event['event_type']
when 'milestone' when 'milestone'
...@@ -37,7 +35,7 @@ class Milestones::BurnupChartService ...@@ -37,7 +35,7 @@ class Milestones::BurnupChartService
end end
end end
chart_data ServiceResponse.success(payload: chart_data)
end end
private private
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Milestone'] do
it { expect(described_class).to have_graphql_field(:burnup_time_series) }
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('Milestones::BurnupChartService::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 GitlabSchema.types['BurnupChartDailyTotals'] do
it { expect(described_class.graphql_name).to eq('BurnupChartDailyTotals') }
it 'has specific fields' do
expect(described_class).to have_graphql_fields(
:date,
:scope_count,
:scope_weight,
:completed_count,
:completed_weight
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Querying a Milestone' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
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(:query) do
graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, fields)
end
subject { graphql_data['milestone'] }
before_all do
project.add_guest(current_user)
end
context 'burnupTimeSeries' do
let(:fields) do
<<~FIELDS
burnupTimeSeries {
date
scopeCount
scopeWeight
completedCount
completedWeight
}
FIELDS
end
let_it_be(:issue) { create(:issue, project: project) }
before_all do
create(:resource_milestone_event, issue: issue, milestone: milestone, action: :add, created_at: '2020-01-05')
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(burnup_charts: true)
end
context 'with insufficient license' do
before do
stub_licensed_features(milestone_charts: false)
end
it 'returns an error' do
post_graphql(query, current_user: current_user)
expect(graphql_errors).to include(a_hash_including('message' => 'Milestone does not support burnup charts'))
end
end
context 'with correct license' do
before do
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
it 'returns burnup chart data' do
post_graphql(query, current_user: current_user)
expect(subject).to eq({
'burnupTimeSeries' => [
{
'date' => '2020-01-05',
'scopeCount' => 1,
'scopeWeight' => 0,
'completedCount' => 0,
'completedWeight' => 0
}
]
})
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(burnup_charts: false)
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
it 'returns empty results' do
post_graphql(query, current_user: current_user)
expect(subject).to eq({ 'burnupTimeSeries' => [] })
end
end
end
end
...@@ -4,249 +4,274 @@ require 'spec_helper' ...@@ -4,249 +4,274 @@ require 'spec_helper'
RSpec.describe Milestones::BurnupChartService do RSpec.describe Milestones::BurnupChartService do
let_it_be(:project) { create(:project) } 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(:milestone, reload: true) { create(:milestone, project: project, start_date: '2020-01-01', due_date: '2020-01-15') }
let_it_be(:issues) { create_list(:issue, 5, project: project) } let_it_be(:issues) { create_list(:issue, 5, project: project) }
let(:chart_data) { described_class.new(milestone).execute } let(:response) { described_class.new(milestone).execute }
it 'raises an error when milestone does not have a start and due date' do context 'when license is not available' do
milestone = build(:milestone, project: project) before do
stub_licensed_features(milestone_charts: false)
end
expect { described_class.new(milestone) }.to raise_error('Milestone must have a start and due date') it 'returns an error message' do
expect(response.error?).to eq(true)
expect(response.message).to eq('Milestone does not support burnup charts')
end
end end
it 'raises an error when the number of events exceeds the limit' do context 'when license is available' do
stub_const('Milestones::BurnupChartService::EVENT_COUNT_LIMIT', 1) before do
stub_licensed_features(milestone_charts: true, issue_weights: true)
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15') end
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
context 'when milestone does not have a start and due date' do
expect { chart_data }.to raise_error(described_class::TooManyEventsError) let(:milestone) { build(:milestone, project: project) }
end
it 'returns an error message' do
it 'aggregates events before the start date to the start date' do expect(response.error?).to eq(true)
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15') expect(response.message).to eq('Milestone must have a start and due date')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2019-12-18') end
end
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2019-12-18') it 'returns an error when the number of events exceeds the limit' do
stub_const('Milestones::BurnupChartService::EVENT_COUNT_LIMIT', 1)
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2019-12-18') create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15')
create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2019-12-25') create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_milestone_event, issue: issues[3], milestone: milestone, action: :add, created_at: '2019-12-17') expect(response.error?).to eq(true)
create(:resource_weight_event, issue: issues[3], weight: 4, created_at: '2019-12-18') expect(response.message).to eq('Burnup chart could not be generated due to too many events')
create(:resource_state_event, issue: issues[3], state: :closed, created_at: '2019-12-26') end
expect(chart_data).to eq([ it 'aggregates events before the start date to the start date' do
{ create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15')
date: Date.parse('2020-01-01'), create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2019-12-18')
scope_count: 4,
scope_weight: 10, create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
completed_count: 2, create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2019-12-18')
completed_weight: 7
} create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2019-12-16')
]) create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2019-12-18')
end create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2019-12-25')
it 'updates counts and weight when the milestone is added or removed' do create(:resource_milestone_event, issue: issues[3], milestone: milestone, action: :add, created_at: '2019-12-17')
# Add milestone to an open issue with no weight. create(:resource_weight_event, issue: issues[3], weight: 4, created_at: '2019-12-18')
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00') create(:resource_state_event, issue: issues[3], state: :closed, created_at: '2019-12-26')
# Ignore duplicate add event.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00') expect(response.success?).to eq(true)
expect(response.payload).to eq([
# Add milestone to an open issue with weight 2 on the same day. This should increment the scope totals for the same day. {
create(:resource_weight_event, issue: issues[1], weight: 2, created_at: '2020-01-01') date: Date.parse('2020-01-01'),
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 05:00') scope_count: 4,
scope_weight: 10,
# Add milestone to already closed issue with weight 3. This should increment both the scope and completed totals. completed_count: 2,
create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2020-01-01') completed_weight: 7
create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2020-01-05') }
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2020-01-06') ])
end
# Remove milestone from the 2nd open issue. This should decrement the scope totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :remove, created_at: '2020-01-07') it 'updates counts and weight when the milestone is added or removed' do
# Add milestone to an open issue with no weight.
# Remove milestone from the closed issue. This should decrement both the scope and completed totals. create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00')
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :remove, created_at: '2020-01-08') # Ignore duplicate add event.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00')
# Adding a different milestone should not affect the data.
create(:resource_milestone_event, issue: issues[3], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-08') # Add milestone to an open issue with weight 2 on the same day. This should increment the scope totals for the same day.
create(:resource_weight_event, issue: issues[1], weight: 2, created_at: '2020-01-01')
# Adding the milestone after the due date should not affect the data. create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 05:00')
create(:resource_milestone_event, issue: issues[4], milestone: milestone, action: :add, created_at: '2020-01-30')
# Add milestone to already closed issue with weight 3. This should increment both the scope and completed totals.
# Removing the milestone after the due date should not affect the data. create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2020-01-01')
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :remove, created_at: '2020-01-30') create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2020-01-05')
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2020-01-06')
expect(chart_data).to eq([
{ # Remove milestone from the 2nd open issue. This should decrement the scope totals.
date: Date.parse('2020-01-05'), create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :remove, created_at: '2020-01-07')
scope_count: 2,
scope_weight: 2, # Remove milestone from the closed issue. This should decrement both the scope and completed totals.
completed_count: 0, create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :remove, created_at: '2020-01-08')
completed_weight: 0
}, # Adding a different milestone should not affect the data.
{ create(:resource_milestone_event, issue: issues[3], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-08')
date: Date.parse('2020-01-06'),
scope_count: 3, # Adding the milestone after the due date should not affect the data.
scope_weight: 5, create(:resource_milestone_event, issue: issues[4], milestone: milestone, action: :add, created_at: '2020-01-30')
completed_count: 1,
completed_weight: 3 # Removing the milestone after the due date should not affect the data.
}, create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :remove, created_at: '2020-01-30')
{
date: Date.parse('2020-01-07'), expect(response.success?).to eq(true)
scope_count: 2, expect(response.payload).to eq([
scope_weight: 3, {
completed_count: 1, date: Date.parse('2020-01-05'),
completed_weight: 3 scope_count: 2,
}, scope_weight: 2,
{ completed_count: 0,
date: Date.parse('2020-01-08'), completed_weight: 0
scope_count: 1, },
scope_weight: 0, {
completed_count: 0, date: Date.parse('2020-01-06'),
completed_weight: 0 scope_count: 3,
} scope_weight: 5,
]) completed_count: 1,
end completed_weight: 3
},
it 'updates the completed counts when issue state is changed' do {
# Close an issue assigned to the milestone with weight 2. This should increment the completed totals. date: Date.parse('2020-01-07'),
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01 01:00') scope_count: 2,
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-01 02:00') scope_weight: 3,
create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-02') completed_count: 1,
completed_weight: 3
# Closing an issue that is already closed should be ignored. },
create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-03') {
date: Date.parse('2020-01-08'),
# Re-opening the issue should decrement the completed totals. scope_count: 1,
create(:resource_state_event, issue: issues[0], state: :reopened, created_at: '2020-01-04') scope_weight: 0,
completed_count: 0,
# Closing and re-opening an issue on the same day should not change the totals. completed_weight: 0
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 01:00') }
create(:resource_weight_event, issue: issues[1], weight: 3, created_at: '2020-01-05 02:00') ])
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-06 05:00') end
create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-06 08:00')
it 'updates the completed counts when issue state is changed' do
# Re-opening an issue that is already open should be ignored. # Close an issue assigned to the milestone with weight 2. This should increment the completed totals.
create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-07') create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01 01:00')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-01 02:00')
# Closing a re-opened issue should increment the completed totals. create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-02')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-08')
# Closing an issue that is already closed should be ignored.
# Changing state when the milestone is already removed should not affect the data. create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-03')
create(:resource_milestone_event, issue: issues[1], action: :remove, created_at: '2020-01-09')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-10') # Re-opening the issue should decrement the completed totals.
create(:resource_state_event, issue: issues[0], state: :reopened, created_at: '2020-01-04')
expect(chart_data).to eq([
{ # Closing and re-opening an issue on the same day should not change the totals.
date: Date.parse('2020-01-01'), create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 01:00')
scope_count: 1, create(:resource_weight_event, issue: issues[1], weight: 3, created_at: '2020-01-05 02:00')
scope_weight: 2, create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-06 05:00')
completed_count: 0, create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-06 08:00')
completed_weight: 0
}, # Re-opening an issue that is already open should be ignored.
{ create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-07')
date: Date.parse('2020-01-02'),
scope_count: 1, # Closing a re-opened issue should increment the completed totals.
scope_weight: 2, create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-08')
completed_count: 1,
completed_weight: 2 # Changing state when the milestone is already removed should not affect the data.
}, create(:resource_milestone_event, issue: issues[1], action: :remove, created_at: '2020-01-09')
{ create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-10')
date: Date.parse('2020-01-04'),
scope_count: 1, expect(response.success?).to eq(true)
scope_weight: 2, expect(response.payload).to eq([
completed_count: 0, {
completed_weight: 0 date: Date.parse('2020-01-01'),
}, scope_count: 1,
{ scope_weight: 2,
date: Date.parse('2020-01-05'), completed_count: 0,
scope_count: 2, completed_weight: 0
scope_weight: 5, },
completed_count: 0, {
completed_weight: 0 date: Date.parse('2020-01-02'),
}, scope_count: 1,
{ scope_weight: 2,
date: Date.parse('2020-01-06'), completed_count: 1,
scope_count: 2, completed_weight: 2
scope_weight: 5, },
completed_count: 0, {
completed_weight: 0 date: Date.parse('2020-01-04'),
}, scope_count: 1,
{ scope_weight: 2,
date: Date.parse('2020-01-08'), completed_count: 0,
scope_count: 2, completed_weight: 0
scope_weight: 5, },
completed_count: 1, {
completed_weight: 3 date: Date.parse('2020-01-05'),
}, scope_count: 2,
{ scope_weight: 5,
date: Date.parse('2020-01-09'), completed_count: 0,
scope_count: 1, completed_weight: 0
scope_weight: 2, },
completed_count: 0, {
completed_weight: 0 date: Date.parse('2020-01-06'),
} scope_count: 2,
]) scope_weight: 5,
end completed_count: 0,
completed_weight: 0
it 'updates the weight totals when issue weight is changed' do },
# Issue starts out with no weight and should increment once the weight is changed to 2. {
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01') date: Date.parse('2020-01-08'),
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-02') scope_count: 2,
scope_weight: 5,
# A closed issue is added and weight is set to 5 and should add to the weight totals. completed_count: 1,
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-03 01:00') completed_weight: 3
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-03 02:00') },
create(:resource_weight_event, issue: issues[1], weight: 5, created_at: '2020-01-03 03:00') {
date: Date.parse('2020-01-09'),
# Lowering the weight of the 2nd issue should decrement the weight totals. scope_count: 1,
create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2020-01-04') scope_weight: 2,
completed_count: 0,
# After the first issue is assigned to another milestone, weight changes shouldn't affect the data. completed_weight: 0
create(:resource_milestone_event, issue: issues[0], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-05') }
create(:resource_weight_event, issue: issues[0], weight: 10, created_at: '2020-01-06') ])
end
expect(chart_data).to eq([
{ it 'updates the weight totals when issue weight is changed' do
date: Date.parse('2020-01-01'), # Issue starts out with no weight and should increment once the weight is changed to 2.
scope_count: 1, create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01')
scope_weight: 0, create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-02')
completed_count: 0,
completed_weight: 0 # A closed issue is added and weight is set to 5 and should add to the weight totals.
}, create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-03 01:00')
{ create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-03 02:00')
date: Date.parse('2020-01-02'), create(:resource_weight_event, issue: issues[1], weight: 5, created_at: '2020-01-03 03:00')
scope_count: 1,
scope_weight: 2, # Lowering the weight of the 2nd issue should decrement the weight totals.
completed_count: 0, create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2020-01-04')
completed_weight: 0
}, # After the first issue is assigned to another milestone, weight changes shouldn't affect the data.
{ create(:resource_milestone_event, issue: issues[0], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-05')
date: Date.parse('2020-01-03'), create(:resource_weight_event, issue: issues[0], weight: 10, created_at: '2020-01-06')
scope_count: 2,
scope_weight: 7, expect(response.success?).to eq(true)
completed_count: 1, expect(response.payload).to eq([
completed_weight: 5 {
}, date: Date.parse('2020-01-01'),
{ scope_count: 1,
date: Date.parse('2020-01-04'), scope_weight: 0,
scope_count: 2, completed_count: 0,
scope_weight: 3, completed_weight: 0
completed_count: 1, },
completed_weight: 1 {
}, date: Date.parse('2020-01-02'),
{ scope_count: 1,
date: Date.parse('2020-01-05'), scope_weight: 2,
scope_count: 1, completed_count: 0,
scope_weight: 1, completed_weight: 0
completed_count: 1, },
completed_weight: 1 {
} date: Date.parse('2020-01-03'),
]) scope_count: 2,
scope_weight: 7,
completed_count: 1,
completed_weight: 5
},
{
date: Date.parse('2020-01-04'),
scope_count: 2,
scope_weight: 3,
completed_count: 1,
completed_weight: 1
},
{
date: Date.parse('2020-01-05'),
scope_count: 1,
scope_weight: 1,
completed_count: 1,
completed_weight: 1
}
])
end
end end
end end
...@@ -4190,6 +4190,9 @@ msgstr "" ...@@ -4190,6 +4190,9 @@ msgstr ""
msgid "Burnup chart" msgid "Burnup chart"
msgstr "" msgstr ""
msgid "Burnup chart could not be generated due to too many events"
msgstr ""
msgid "Business" msgid "Business"
msgstr "" msgstr ""
...@@ -15705,12 +15708,18 @@ msgid_plural "Milestones" ...@@ -15705,12 +15708,18 @@ msgid_plural "Milestones"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Milestone does not support burnup charts"
msgstr ""
msgid "Milestone lists not available with your current license" msgid "Milestone lists not available with your current license"
msgstr "" msgstr ""
msgid "Milestone lists show all issues from the selected milestone." msgid "Milestone lists show all issues from the selected milestone."
msgstr "" msgstr ""
msgid "Milestone must have a start and due date"
msgstr ""
msgid "MilestoneSidebar|Closed:" msgid "MilestoneSidebar|Closed:"
msgstr "" msgstr ""
......
...@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do ...@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do
stats stats
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields).at_least
end end
describe 'stats field' do describe 'stats field' do
......
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