Commit d20bb41b authored by Adam Hegyi's avatar Adam Hegyi

Add sorting options to value stream analytics

This change adds sorting capabilities when listing the records for the
VSA stage.
parent d648b282
...@@ -16,7 +16,7 @@ module Gitlab ...@@ -16,7 +16,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def load def load
order_by_end_event(query) order_by(query, :end_event, :desc)
.select(round_duration_to_seconds.as('duration_in_seconds'), stage.end_event.timestamp_projection.as('finished_at')) .select(round_duration_to_seconds.as('duration_in_seconds'), stage.end_event.timestamp_projection.as('finished_at'))
.limit(MAX_RESULTS) .limit(MAX_RESULTS)
end end
......
...@@ -16,6 +16,8 @@ module Gitlab ...@@ -16,6 +16,8 @@ module Gitlab
:created_after, :created_after,
:author_username, :author_username,
:milestone_title, :milestone_title,
:sort,
:direction,
label_name: [].freeze, label_name: [].freeze,
assignee_username: [].freeze, assignee_username: [].freeze,
project_ids: [].freeze project_ids: [].freeze
...@@ -35,6 +37,8 @@ module Gitlab ...@@ -35,6 +37,8 @@ module Gitlab
attribute :group attribute :group
attribute :current_user attribute :current_user
attribute :value_stream attribute :value_stream
attribute :sort
attribute :direction
FINDER_PARAM_NAMES.each do |param_name| FINDER_PARAM_NAMES.each do |param_name|
attribute param_name attribute param_name
...@@ -62,7 +66,9 @@ module Gitlab ...@@ -62,7 +66,9 @@ module Gitlab
current_user: current_user, current_user: current_user,
from: created_after, from: created_after,
to: created_before, to: created_before,
project_ids: project_ids project_ids: project_ids,
sort: sort&.to_sym,
direction: direction&.to_sym
}.merge(attributes.symbolize_keys.slice(*FINDER_PARAM_NAMES)) }.merge(attributes.symbolize_keys.slice(*FINDER_PARAM_NAMES))
end end
...@@ -77,6 +83,8 @@ module Gitlab ...@@ -77,6 +83,8 @@ module Gitlab
attrs[:assignees] = assignee_username.to_json if assignee_username.present? attrs[:assignees] = assignee_username.to_json if assignee_username.present?
attrs[:author] = author_username if author_username.present? attrs[:author] = author_username if author_username.present?
attrs[:milestone] = milestone_title if milestone_title.present? attrs[:milestone] = milestone_title if milestone_title.present?
attrs[:sort] = sort if sort.present?
attrs[:direction] = direction if direction.present?
end end
end end
......
...@@ -22,33 +22,43 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::DataCollector do ...@@ -22,33 +22,43 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::DataCollector do
let(:params) { { from: Time.new(2019), to: Time.new(2020), current_user: user } } let(:params) { { from: Time.new(2019), to: Time.new(2020), current_user: user } }
let(:data_collector) { described_class.new(stage: stage, params: params) } let(:data_collector) { described_class.new(stage: stage, params: params) }
before do let!(:resource1) do
# takes 10 days # takes 10 days
resource1 = travel_to(Time.new(2019, 3, 5)) do resource = travel_to(Time.new(2019, 3, 5)) do
create_data_for_start_event(self) create_data_for_start_event(self)
end end
travel_to(Time.new(2019, 3, 15)) do travel_to(Time.new(2019, 3, 15)) do
create_data_for_end_event(resource1, self) create_data_for_end_event(resource, self)
end end
resource
end
let!(:resource2) do
# takes 5 days # takes 5 days
resource2 = travel_to(Time.new(2019, 3, 5)) do resource = travel_to(Time.new(2019, 3, 5)) do
create_data_for_start_event(self) create_data_for_start_event(self)
end end
travel_to(Time.new(2019, 3, 10)) do travel_to(Time.new(2019, 3, 10)) do
create_data_for_end_event(resource2, self) create_data_for_end_event(resource, self)
end end
resource
end
let!(:resource3) do
# takes 15 days # takes 15 days
resource3 = travel_to(Time.new(2019, 3, 5)) do resource = travel_to(Time.new(2019, 3, 5)) do
create_data_for_start_event(self) create_data_for_start_event(self)
end end
travel_to(Time.new(2019, 3, 20)) do travel_to(Time.new(2019, 3, 20)) do
create_data_for_end_event(resource3, self) create_data_for_end_event(resource, self)
end end
resource
end end
it 'loads serialized records' do it 'loads serialized records' do
...@@ -56,6 +66,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::DataCollector do ...@@ -56,6 +66,20 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::DataCollector do
expect(items.size).to eq(3) expect(items.size).to eq(3)
end end
context 'when sorting by duration' do
before do
params[:sort] = :duration
params[:direction] = :desc
end
it 'returns serialized records sorted by duration DESC' do
expected_ordered_iids = [resource3.iid, resource1.iid, resource2.iid]
iids = data_collector.serialized_records.map { |record| record[:iid].to_i }
expect(iids).to eq(expected_ordered_iids)
end
end
it 'calculates median' do it 'calculates median' do
expect(round_to_days(data_collector.median.seconds)).to eq(10) expect(round_to_days(data_collector.median.seconds)).to eq(10)
end end
......
...@@ -205,4 +205,24 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams do ...@@ -205,4 +205,24 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams do
it { expect(subject[:labels]).to eq('["label1","label2"]') } it { expect(subject[:labels]).to eq('["label1","label2"]') }
it { expect(subject[:author]).to eq('author') } it { expect(subject[:author]).to eq('author') }
end end
describe 'sorting params' do
before do
params.merge!(sort: 'duration', direction: 'asc')
end
it 'converts sorting params to symbol when passing it to data collector' do
data_collector_params = subject.to_data_collector_params
expect(data_collector_params[:sort]).to eq(:duration)
expect(data_collector_params[:direction]).to eq(:asc)
end
it 'adds corting params to data attributes' do
data_attributes = subject.to_data_attributes
expect(data_attributes[:sort]).to eq('duration')
expect(data_attributes[:direction]).to eq('asc')
end
end
end end
...@@ -220,6 +220,20 @@ RSpec.shared_examples 'Value Stream Analytics Stages controller' do ...@@ -220,6 +220,20 @@ RSpec.shared_examples 'Value Stream Analytics Stages controller' do
include_examples 'Value Stream Analytics data endpoint examples' include_examples 'Value Stream Analytics data endpoint examples'
include_examples 'group permission check on the controller level' include_examples 'group permission check on the controller level'
context 'sort params' do
before do
params.merge!(sort: 'duration', direction: 'asc')
end
it 'accepts sort params' do
expect(Gitlab::Analytics::CycleAnalytics::Sorting).to receive(:apply).with(kind_of(ActiveRecord::Relation), kind_of(Analytics::CycleAnalytics::GroupStage), :duration, :asc).and_call_original
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
end end
describe 'GET #duration_chart' do describe 'GET #duration_chart' do
......
...@@ -29,6 +29,8 @@ module Gitlab ...@@ -29,6 +29,8 @@ module Gitlab
@stage = stage @stage = stage
@query = query @query = query
@params = params @params = params
@sort = params[:sort] || :end_event
@direction = params[:direction] || :desc
end end
def serialized_records def serialized_records
...@@ -52,7 +54,7 @@ module Gitlab ...@@ -52,7 +54,7 @@ module Gitlab
private private
attr_reader :stage, :query, :params attr_reader :stage, :query, :params, :sort, :direction
def columns def columns
MAPPINGS.fetch(subject_class).fetch(:columns_for_select).map do |column_name| MAPPINGS.fetch(subject_class).fetch(:columns_for_select).map do |column_name|
...@@ -90,7 +92,7 @@ module Gitlab ...@@ -90,7 +92,7 @@ module Gitlab
end end
def ordered_and_limited_query def ordered_and_limited_query
order_by_end_event(query, columns).limit(MAX_RECORDS) order_by(query, sort, direction, columns).limit(MAX_RECORDS)
end end
def records def records
......
# frozen_string_literal: true
module Gitlab
module Analytics
module CycleAnalytics
class Sorting
# rubocop: disable CodeReuse/ActiveRecord
SORTING_OPTIONS = {
end_event: {
asc: -> (query, stage) { query.reorder(stage.end_event.timestamp_projection.asc) },
desc: -> (query, stage) { query.reorder(stage.end_event.timestamp_projection.desc) }
}.freeze,
duration: {
asc: -> (query, stage) { query.reorder(Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).asc) },
desc: -> (query, stage) { query.reorder(Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).desc) }
}.freeze
}.freeze
# rubocop: enable CodeReuse/ActiveRecord,
def self.apply(query, stage, sort, direction)
sort_lambda = SORTING_OPTIONS.dig(sort, direction) || SORTING_OPTIONS.dig(:end_event, :desc)
sort_lambda.call(query, stage)
end
end
end
end
end
...@@ -24,8 +24,8 @@ module Gitlab ...@@ -24,8 +24,8 @@ module Gitlab
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def order_by_end_event(query, extra_columns_to_select = [:id]) def order_by(query, sort, direction, extra_columns_to_select = [:id])
ordered_query = query.reorder(stage.end_event.timestamp_projection.desc) ordered_query = Gitlab::Analytics::CycleAnalytics::Sorting.apply(query, stage, sort, direction)
# When filtering for more than one label, postgres requires the columns in ORDER BY to be present in the GROUP BY clause # When filtering for more than one label, postgres requires the columns in ORDER BY to be present in the GROUP BY clause
if requires_grouping? if requires_grouping?
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Sorting do
let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:order_values) { described_class.apply(MergeRequest.joins(:metrics), stage, sort, direction).order_values }
context 'when invalid sorting params are given' do
let(:sort) { :unknown_sort }
let(:direction) { :unknown_direction }
it 'falls back to end_event DESC sorting' do
expect(order_values).to eq([stage.end_event.timestamp_projection.desc])
end
end
context 'sorting end_event' do
let(:sort) { :end_event }
context 'direction desc' do
let(:direction) { :desc }
specify do
expect(order_values).to eq([stage.end_event.timestamp_projection.desc])
end
end
context 'direction asc' do
let(:direction) { :asc }
specify do
expect(order_values).to eq([stage.end_event.timestamp_projection.asc])
end
end
end
context 'sorting duration' do
let(:sort) { :duration }
context 'direction desc' do
let(:direction) { :desc }
specify do
expect(order_values).to eq([Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).desc])
end
end
context 'direction asc' do
let(:direction) { :asc }
specify do
expect(order_values).to eq([Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).asc])
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