Preload ancestors for subgroups matching filter
Otherwise we'd only preload the ancestors that would fit the page. That way, when a search result was on the first page, but the ancestor didn't fit the page anymore. We would not have the preloaded ancestor when rendering the hierarchy.
This commit is contained in:
parent
2c3c5b3554
commit
26c4e47a0d
3 changed files with 60 additions and 12 deletions
|
@ -27,12 +27,16 @@ class GroupDescendantsFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
# The children array might be extended with the ancestors of projects when
|
# The children array might be extended with the ancestors of projects and
|
||||||
# filtering. In that case, take the maximum so the array does not get limited
|
# subgroups when filtering. In that case, take the maximum so the array does
|
||||||
# Otherwise, allow paginating through all results
|
# not get limited otherwise, allow paginating through all results.
|
||||||
#
|
#
|
||||||
all_required_elements = children
|
all_required_elements = children
|
||||||
all_required_elements |= ancestors_for_projects if params[:filter]
|
if params[:filter]
|
||||||
|
all_required_elements |= ancestors_of_filtered_subgroups
|
||||||
|
all_required_elements |= ancestors_of_filtered_projects
|
||||||
|
end
|
||||||
|
|
||||||
total_count = [all_required_elements.size, paginator.total_count].max
|
total_count = [all_required_elements.size, paginator.total_count].max
|
||||||
|
|
||||||
Kaminari.paginate_array(all_required_elements, total_count: total_count)
|
Kaminari.paginate_array(all_required_elements, total_count: total_count)
|
||||||
|
@ -49,8 +53,11 @@ class GroupDescendantsFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginator
|
def paginator
|
||||||
@paginator ||= Gitlab::MultiCollectionPaginator.new(subgroups, projects,
|
@paginator ||= Gitlab::MultiCollectionPaginator.new(
|
||||||
per_page: params[:per_page])
|
subgroups,
|
||||||
|
projects.with_route,
|
||||||
|
per_page: params[:per_page]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def direct_child_groups
|
def direct_child_groups
|
||||||
|
@ -94,15 +101,21 @@ class GroupDescendantsFinder
|
||||||
#
|
#
|
||||||
# So when searching 'project', on the 'subgroup' page we want to preload
|
# So when searching 'project', on the 'subgroup' page we want to preload
|
||||||
# 'nested-group' but not 'subgroup' or 'root'
|
# 'nested-group' but not 'subgroup' or 'root'
|
||||||
def ancestors_for_groups(base_for_ancestors)
|
def ancestors_of_groups(base_for_ancestors)
|
||||||
Gitlab::GroupHierarchy.new(base_for_ancestors)
|
group_ids = base_for_ancestors.except(:select, :sort).select(:id)
|
||||||
|
Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
|
||||||
.base_and_ancestors(upto: parent_group.id)
|
.base_and_ancestors(upto: parent_group.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ancestors_for_projects
|
def ancestors_of_filtered_projects
|
||||||
projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
|
projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
|
||||||
groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
|
groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
|
||||||
ancestors_for_groups(groups_to_load_ancestors_of)
|
ancestors_of_groups(groups_to_load_ancestors_of)
|
||||||
|
.with_selects_for_list(archived: params[:archived])
|
||||||
|
end
|
||||||
|
|
||||||
|
def ancestors_of_filtered_subgroups
|
||||||
|
ancestors_of_groups(subgroups)
|
||||||
.with_selects_for_list(archived: params[:archived])
|
.with_selects_for_list(archived: params[:archived])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -112,7 +125,7 @@ class GroupDescendantsFinder
|
||||||
# When filtering subgroups, we want to find all matches withing the tree of
|
# When filtering subgroups, we want to find all matches withing the tree of
|
||||||
# descendants to show to the user
|
# descendants to show to the user
|
||||||
groups = if params[:filter]
|
groups = if params[:filter]
|
||||||
ancestors_for_groups(subgroups_matching_filter)
|
subgroups_matching_filter
|
||||||
else
|
else
|
||||||
direct_child_groups
|
direct_child_groups
|
||||||
end
|
end
|
||||||
|
|
|
@ -160,6 +160,30 @@ describe Groups::ChildrenController do
|
||||||
expect(json_response).to eq([])
|
expect(json_response).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'succeeds if multiple pages contain matching subgroups' do
|
||||||
|
create(:group, parent: group, name: 'subgroup-filter-1')
|
||||||
|
create(:group, parent: group, name: 'subgroup-filter-2')
|
||||||
|
|
||||||
|
# Creating the group-to-nest first so it would be loaded into the
|
||||||
|
# relation first before it's parents, this is what would cause the
|
||||||
|
# crash in: https://gitlab.com/gitlab-org/gitlab-ce/issues/40785.
|
||||||
|
#
|
||||||
|
# If we create the parent groups first, those would be loaded into the
|
||||||
|
# collection first, and the pagination would cut off the actual search
|
||||||
|
# result. In this case the hierarchy can be rendered without crashing,
|
||||||
|
# it's just incomplete.
|
||||||
|
group_to_nest = create(:group, parent: group, name: 'subsubgroup-filter-3')
|
||||||
|
subgroup = create(:group, parent: group)
|
||||||
|
3.times do |i|
|
||||||
|
subgroup = create(:group, parent: subgroup)
|
||||||
|
end
|
||||||
|
group_to_nest.update!(parent: subgroup)
|
||||||
|
|
||||||
|
get :index, group_id: group.to_param, filter: 'filter', per_page: 3, format: :json
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
it 'includes pagination headers' do
|
it 'includes pagination headers' do
|
||||||
2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") }
|
2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") }
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ describe GroupDescendantsFinder do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a filter' do
|
context 'with a filter' do
|
||||||
let(:params) { { filter: 'tes' } }
|
let(:params) { { filter: 'test' } }
|
||||||
|
|
||||||
it 'includes only projects matching the filter' do
|
it 'includes only projects matching the filter' do
|
||||||
_other_project = create(:project, namespace: group)
|
_other_project = create(:project, namespace: group)
|
||||||
|
@ -198,6 +198,17 @@ describe GroupDescendantsFinder do
|
||||||
expect(finder.execute).to contain_exactly(subgroup, matching_project)
|
expect(finder.execute).to contain_exactly(subgroup, matching_project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a small page size' do
|
||||||
|
let(:params) { { filter: 'test', per_page: 1 } }
|
||||||
|
|
||||||
|
it 'contains all the ancestors of a matching subgroup regardless the page size' do
|
||||||
|
subgroup = create(:group, :private, parent: group)
|
||||||
|
matching = create(:group, :private, name: 'testgroup', parent: subgroup)
|
||||||
|
|
||||||
|
expect(finder.execute).to contain_exactly(subgroup, matching)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not include the parent itself' do
|
it 'does not include the parent itself' do
|
||||||
group.update!(name: 'test')
|
group.update!(name: 'test')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue