Commit fe2896f6 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch '11096-insights-charts-display-all-periods' into 'master'

Resolve "Insights charts should display all the periods even if there are no data for them"

Closes #11096

See merge request gitlab-org/gitlab-ee!10733
parents 4c4a11aa c21eb117
...@@ -39,25 +39,31 @@ module InsightsActions ...@@ -39,25 +39,31 @@ module InsightsActions
end end
def insights_json def insights_json
issuables = find_issuables(params[:query]) issuables_finder = finder(params[:query])
insights = reduce(issuables, params[:chart_type], params[:query]) issuables = issuables_finder.find
insights = reduce(
issuables: issuables,
chart_type: params[:chart_type],
period: params[:query][:group_by],
period_limit: issuables_finder.period_limit,
labels: params[:query][:collection_labels])
serializer(params[:chart_type]).present(insights) serializer(params[:chart_type]).present(insights)
end end
def find_issuables(query) def reduce(issuables:, chart_type:, period:, period_limit:, labels: nil)
Gitlab::Insights::Finders::IssuableFinder
.new(insights_entity, current_user, query).find
end
def reduce(issuables, chart_type, query)
case chart_type case chart_type
when 'stacked-bar', 'line' when 'stacked-bar', 'line'
Gitlab::Insights::Reducers::LabelCountPerPeriodReducer.reduce(issuables, period: query[:group_by], labels: query[:collection_labels]) Gitlab::Insights::Reducers::LabelCountPerPeriodReducer.reduce(issuables, period: period, period_limit: period_limit, labels: labels)
when 'bar' when 'bar'
Gitlab::Insights::Reducers::CountPerPeriodReducer.reduce(issuables, period: query[:group_by]) Gitlab::Insights::Reducers::CountPerPeriodReducer.reduce(issuables, period: period, period_limit: period_limit)
end end
end end
def finder(query)
Gitlab::Insights::Finders::IssuableFinder
.new(insights_entity, current_user, query)
end
def serializer(chart_type) def serializer(chart_type)
case chart_type case chart_type
when 'stacked-bar' when 'stacked-bar'
......
---
title: Ensure Insights charts show all periods even if there are no data
merge_request: 10733
author:
type: fixed
...@@ -36,6 +36,19 @@ module Gitlab ...@@ -36,6 +36,19 @@ module Gitlab
relation relation
end end
def period_limit
@period_limit ||=
if opts.key?(:period_limit)
begin
Integer(opts[:period_limit])
rescue ArgumentError
raise InvalidPeriodLimitError, "Invalid `:period_limit` option: `#{opts[:period_limit]}`. Expected an integer!"
end
else
PERIODS.dig(period, :default)
end
end
private private
attr_reader :entity, :current_user, :opts attr_reader :entity, :current_user, :opts
...@@ -86,19 +99,6 @@ module Gitlab ...@@ -86,19 +99,6 @@ module Gitlab
period period
end end
end end
def period_limit
@period_limit ||=
if opts.key?(:period_limit)
begin
Integer(opts[:period_limit])
rescue ArgumentError
raise InvalidPeriodLimitError, "Invalid `:period_limit` option: `#{opts[:period_limit]}`. Expected an integer!"
end
else
PERIODS.dig(period, :default)
end
end
end end
end end
end end
......
...@@ -6,13 +6,15 @@ module Gitlab ...@@ -6,13 +6,15 @@ module Gitlab
class CountPerPeriodReducer < BaseReducer class CountPerPeriodReducer < BaseReducer
InvalidPeriodError = Class.new(BaseReducerError) InvalidPeriodError = Class.new(BaseReducerError)
InvalidPeriodFieldError = Class.new(BaseReducerError) InvalidPeriodFieldError = Class.new(BaseReducerError)
InvalidPeriodLimitError = Class.new(BaseReducerError)
VALID_PERIOD = %w[day week month].freeze VALID_PERIOD = %w[day week month].freeze
VALID_PERIOD_FIELD = %i[created_at].freeze VALID_PERIOD_FIELD = %i[created_at].freeze
def initialize(issuables, period:, period_field: :created_at) def initialize(issuables, period:, period_limit:, period_field: :created_at)
super(issuables) super(issuables)
@period = period.to_s.singularize @period = period.to_s.singularize
@period_limit = period_limit.to_i
@period_field = period_field @period_field = period_field
validate! validate!
...@@ -25,14 +27,15 @@ module Gitlab ...@@ -25,14 +27,15 @@ module Gitlab
# 'March 2019' => 1 # 'March 2019' => 1
# } # }
def reduce def reduce
issuables_grouped_by_normalized_period.each_with_object({}) do |(period, issuables), hash| (0...period_limit).reverse_each.each_with_object({}) do |period_ago, hash|
hash[period.strftime(period_format)] = value_for_period(issuables) period_time = normalized_time(period_ago.public_send(period).ago) # rubocop:disable GitlabSecurity/PublicSend
hash[period_time.strftime(period_format)] = value_for_period(issuables_grouped_by_normalized_period.fetch(period_time, []))
end end
end end
private private
attr_reader :period, :period_field attr_reader :period, :period_limit, :period_field
def validate! def validate!
unless VALID_PERIOD.include?(period) unless VALID_PERIOD.include?(period)
...@@ -42,6 +45,10 @@ module Gitlab ...@@ -42,6 +45,10 @@ module Gitlab
unless VALID_PERIOD_FIELD.include?(period_field) unless VALID_PERIOD_FIELD.include?(period_field)
raise InvalidPeriodFieldError, "Invalid value for `period_field`: `#{period_field}`. Allowed values are #{VALID_PERIOD_FIELD}!" raise InvalidPeriodFieldError, "Invalid value for `period_field`: `#{period_field}`. Allowed values are #{VALID_PERIOD_FIELD}!"
end end
unless period_limit > 0
raise InvalidPeriodLimitError, "Invalid value for `period_limit`: `#{period_limit}`. Value must be greater than 0!"
end
end end
# Returns a hash { period => [array of issuables] }, e.g. # Returns a hash { period => [array of issuables] }, e.g.
...@@ -51,11 +58,15 @@ module Gitlab ...@@ -51,11 +58,15 @@ module Gitlab
# #<Fri, 01 Mar 2019 00:00:00 UTC +00:00> => [#<Issue id:3 namespace1/project1#3>] # #<Fri, 01 Mar 2019 00:00:00 UTC +00:00> => [#<Issue id:3 namespace1/project1#3>]
# } # }
def issuables_grouped_by_normalized_period def issuables_grouped_by_normalized_period
issuables.group_by do |issuable| @issuables_grouped_by_normalized_period ||= issuables.group_by do |issuable|
issuable.public_send(period_field).public_send(period_normalizer) # rubocop:disable GitlabSecurity/PublicSend normalized_time(issuable.public_send(period_field)) # rubocop:disable GitlabSecurity/PublicSend
end end
end end
def normalized_time(time)
time.public_send(period_normalizer) # rubocop:disable GitlabSecurity/PublicSend
end
def period_normalizer def period_normalizer
:"beginning_of_#{period}" :"beginning_of_#{period}"
end end
......
...@@ -17,8 +17,8 @@ module Gitlab ...@@ -17,8 +17,8 @@ module Gitlab
# } # }
# } # }
class LabelCountPerPeriodReducer < CountPerPeriodReducer class LabelCountPerPeriodReducer < CountPerPeriodReducer
def initialize(issuables, labels:, period:, period_field: :created_at) def initialize(issuables, labels:, period:, period_limit:, period_field: :created_at)
super(issuables, period: period, period_field: period_field) super(issuables, period: period, period_limit: period_limit, period_field: period_field)
@labels = labels @labels = labels
end end
......
...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerLabelReducer do ...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerLabelReducer do
filter_labels: [label_bug.title], filter_labels: [label_bug.title],
collection_labels: [label_manage.title, label_plan.title], collection_labels: [label_manage.title, label_plan.title],
group_by: 'month', group_by: 'month',
period_limit: 2 period_limit: 5
} }
end end
let(:issuable_relation) { find_issuables(project, opts) } let(:issuable_relation) { find_issuables(project, opts) }
......
...@@ -9,8 +9,8 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do ...@@ -9,8 +9,8 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
Gitlab::Insights::Finders::IssuableFinder.new(project, nil, opts).find Gitlab::Insights::Finders::IssuableFinder.new(project, nil, opts).find
end end
def reduce(issuable_relation, period, period_field = :created_at) def reduce(issuable_relation, period, period_limit = 5, period_field = :created_at)
described_class.reduce(issuable_relation, period: period, period_field: period_field) described_class.reduce(issuable_relation, period: period, period_limit: period_limit, period_field: period_field)
end end
let(:opts) do let(:opts) do
...@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do ...@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
issuable_type: 'issue', issuable_type: 'issue',
filter_labels: [label_bug.title], filter_labels: [label_bug.title],
group_by: 'month', group_by: 'month',
period_limit: 3 period_limit: 5
} }
end end
let(:issuable_relation) { find_issuables(project, opts) } let(:issuable_relation) { find_issuables(project, opts) }
...@@ -29,8 +29,10 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do ...@@ -29,8 +29,10 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
let(:expected) do let(:expected) do
{ {
'January 2019' => 1, 'January 2019' => 1,
'February 2019' => 1, 'February 2019' => 0,
'March 2019' => 1 'March 2019' => 1,
'April 2019' => 1,
'May 2019' => 0
} }
end end
...@@ -39,7 +41,11 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do ...@@ -39,7 +41,11 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
end end
it 'raises an error for an unknown :period_field option' do it 'raises an error for an unknown :period_field option' do
expect { reduce(issuable_relation, 'month', :foo) }.to raise_error(described_class::InvalidPeriodFieldError, "Invalid value for `period_field`: `foo`. Allowed values are #{described_class::VALID_PERIOD_FIELD}!") expect { reduce(issuable_relation, 'month', 5, :foo) }.to raise_error(described_class::InvalidPeriodFieldError, "Invalid value for `period_field`: `foo`. Allowed values are #{described_class::VALID_PERIOD_FIELD}!")
end
it 'raises an error for an unknown :period_limit option' do
expect { reduce(issuable_relation, 'month', -1) }.to raise_error(described_class::InvalidPeriodLimitError, "Invalid value for `period_limit`: `-1`. Value must be greater than 0!")
end end
it 'returns issuables with only the needed fields' do it 'returns issuables with only the needed fields' do
......
...@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do ...@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do
end end
def reduce(issuable_relation, period, labels) def reduce(issuable_relation, period, labels)
described_class.reduce(issuable_relation, period: period, labels: labels) described_class.reduce(issuable_relation, period: period, period_limit: 5, labels: labels)
end end
let(:opts) do let(:opts) do
...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do ...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do
filter_labels: [label_bug.title], filter_labels: [label_bug.title],
collection_labels: [label_manage.title, label_plan.title], collection_labels: [label_manage.title, label_plan.title],
group_by: 'month', group_by: 'month',
period_limit: 3 period_limit: 5
} }
end end
let(:issuable_relation) { find_issuables(project, opts) } let(:issuable_relation) { find_issuables(project, opts) }
...@@ -35,14 +35,24 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do ...@@ -35,14 +35,24 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do
Gitlab::Insights::UNCATEGORIZED => 1 Gitlab::Insights::UNCATEGORIZED => 1
}, },
'February 2019' => { 'February 2019' => {
label_manage.title => 1, label_manage.title => 0,
label_plan.title => 0, label_plan.title => 0,
Gitlab::Insights::UNCATEGORIZED => 0 Gitlab::Insights::UNCATEGORIZED => 0
}, },
'March 2019' => { 'March 2019' => {
label_manage.title => 1,
label_plan.title => 0,
Gitlab::Insights::UNCATEGORIZED => 0
},
'April 2019' => {
label_manage.title => 0, label_manage.title => 0,
label_plan.title => 1, label_plan.title => 1,
Gitlab::Insights::UNCATEGORIZED => 0 Gitlab::Insights::UNCATEGORIZED => 0
},
'May 2019' => {
label_manage.title => 0,
label_plan.title => 0,
Gitlab::Insights::UNCATEGORIZED => 0
} }
} }
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
RSpec.shared_context 'Insights reducers context' do RSpec.shared_context 'Insights reducers context' do
around do |example| around do |example|
Timecop.freeze(Time.utc(2019, 3, 5)) { example.run } Timecop.freeze(Time.utc(2019, 5, 5)) { example.run }
end end
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
...@@ -11,6 +11,6 @@ RSpec.shared_context 'Insights reducers context' do ...@@ -11,6 +11,6 @@ RSpec.shared_context 'Insights reducers context' do
let(:label_plan) { create(:label, project: project, name: 'Plan') } let(:label_plan) { create(:label, project: project, name: 'Plan') }
let!(:issuable0) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 1, 5), project: project) } let!(:issuable0) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 1, 5), project: project) }
let!(:issuable1) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 1, 5), labels: [label_bug], project: project) } let!(:issuable1) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 1, 5), labels: [label_bug], project: project) }
let!(:issuable2) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 2, 5), labels: [label_bug, label_manage, label_plan], project: project) } let!(:issuable2) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug, label_manage, label_plan], project: project) }
let!(:issuable3) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug, label_plan], project: project) } let!(:issuable3) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 4, 5), labels: [label_bug, label_plan], project: project) }
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