Commit 6ca84a86 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Limit full path search to certain places only

Full path search times out when searching all projects so
we only do this when the search space is already limited
by a group or user's authorized projects
parent c25b124e
...@@ -64,6 +64,7 @@ export default { ...@@ -64,6 +64,7 @@ export default {
this.groupId, this.groupId,
term, term,
{ {
search_namespaces: true,
with_issues_enabled: true, with_issues_enabled: true,
with_shared: false, with_shared: false,
include_subgroups: true, include_subgroups: true,
......
...@@ -54,6 +54,7 @@ const projectSelect = () => { ...@@ -54,6 +54,7 @@ const projectSelect = () => {
this.groupId, this.groupId,
query.term, query.term,
{ {
search_namespaces: true,
with_issues_enabled: this.withIssuesEnabled, with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled, with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared, with_shared: this.withShared,
......
...@@ -25,7 +25,7 @@ module Autocomplete ...@@ -25,7 +25,7 @@ module Autocomplete
def execute def execute
current_user current_user
.projects_where_can_admin_issues .projects_where_can_admin_issues
.optionally_search(search) .optionally_search(search, include_namespace: true)
.excluding_project(project_id) .excluding_project(project_id)
.eager_load_namespace_and_owner .eager_load_namespace_and_owner
.sorted_by_name_asc_limited(LIMIT) .sorted_by_name_asc_limited(LIMIT)
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
# tags: string[] # tags: string[]
# personal: boolean # personal: boolean
# search: string # search: string
# search_namespaces: boolean
# non_archived: boolean # non_archived: boolean
# archived: 'only' or boolean # archived: 'only' or boolean
# min_access_level: integer # min_access_level: integer
...@@ -171,7 +172,7 @@ class ProjectsFinder < UnionFinder ...@@ -171,7 +172,7 @@ class ProjectsFinder < UnionFinder
def by_search(items) def by_search(items)
params[:search] ||= params[:name] params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end end
def by_deleted_status(items) def by_deleted_status(items)
......
...@@ -12,8 +12,8 @@ module OptionallySearch ...@@ -12,8 +12,8 @@ module OptionallySearch
end end
# Optionally limits a result set to those matching the given search query. # Optionally limits a result set to those matching the given search query.
def optionally_search(query = nil) def optionally_search(query = nil, **options)
query.present? ? search(query) : all query.present? ? search(query, **options) : all
end end
end end
end end
...@@ -591,9 +591,9 @@ class Project < ApplicationRecord ...@@ -591,9 +591,9 @@ class Project < ApplicationRecord
# case-insensitive. # case-insensitive.
# #
# query - The search query as a String. # query - The search query as a String.
def search(query) def search(query, include_namespace: false)
if Feature.enabled?(:project_search_by_full_path, default_enabled: true) if include_namespace && Feature.enabled?(:project_search_by_full_path, default_enabled: true)
joins(:route).fuzzy_search(query, [Route.arel_table[:path], :name, :description]) joins(:route).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name], :description])
else else
fuzzy_search(query, [:path, :name, :description]) fuzzy_search(query, [:path, :name, :description])
end end
......
---
title: Only enable searching of projects by full path / name on certain dropdowns
merge_request: 21910
author:
type: changed
...@@ -46,6 +46,7 @@ GET /projects ...@@ -46,6 +46,7 @@ GET /projects
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria | | `search` | string | no | Return list of projects matching the search criteria |
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` |
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | | `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
| `owned` | boolean | no | Limit by projects explicitly owned by the current user | | `owned` | boolean | no | Limit by projects explicitly owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of | | `membership` | boolean | no | Limit by projects that the current user is a member of |
......
...@@ -505,6 +505,7 @@ module API ...@@ -505,6 +505,7 @@ module API
finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility] finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
finder_params[:archived] = archived_param unless params[:archived].nil? finder_params[:archived] = archived_param unless params[:archived].nil?
finder_params[:search] = params[:search] if params[:search] finder_params[:search] = params[:search] if params[:search]
finder_params[:search_namespaces] = true if params[:search_namespaces].present?
finder_params[:user] = params.delete(:user) if params[:user] finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes] finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
......
...@@ -63,6 +63,7 @@ module API ...@@ -63,6 +63,7 @@ module API
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility' desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of projects matching the search criteria' optional :search, type: String, desc: 'Return list of projects matching the search criteria'
optional :search_namespaces, type: Boolean, desc: "Include ancestor namespaces when matching search criteria"
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of' optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
......
...@@ -100,6 +100,8 @@ describe 'Group issues page' do ...@@ -100,6 +100,8 @@ describe 'Group issues page' do
find('.empty-state .js-lazy-loaded') find('.empty-state .js-lazy-loaded')
find('.new-project-item-link').click find('.new-project-item-link').click
find('.select2-input').set(group.name)
page.within('.select2-results') do page.within('.select2-results') do
expect(page).to have_content(project.full_name) expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_issues_disabled.full_name) expect(page).not_to have_content(project_with_issues_disabled.full_name)
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Autocomplete::MoveToProjectFinder do describe Autocomplete::MoveToProjectFinder do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:no_access_project) { create(:project) } let(:no_access_project) { create(:project) }
let(:guest_project) { create(:project) } let(:guest_project) { create(:project) }
...@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do ...@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do
expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a)
.to eq([wadus_project]) .to eq([wadus_project])
end end
it 'allows searching by parent namespace' do
group = create(:group)
other_project = create(:project, group: group)
other_project.add_maintainer(user)
expect(described_class.new(user, project_id: project.id, search: group.name).execute.to_a)
.to contain_exactly(other_project)
end
end end
end end
end end
...@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
include AdminModeHelper include AdminModeHelper
describe '#execute' do describe '#execute' do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
let!(:private_project) do let_it_be(:private_project) do
create(:project, :private, name: 'A', path: 'A') create(:project, :private, name: 'A', path: 'A')
end end
let!(:internal_project) do let_it_be(:internal_project) do
create(:project, :internal, group: group, name: 'B', path: 'B') create(:project, :internal, group: group, name: 'B', path: 'B')
end end
let!(:public_project) do let_it_be(:public_project) do
create(:project, :public, group: group, name: 'C', path: 'C') create(:project, :public, group: group, name: 'C', path: 'C')
end end
let!(:shared_project) do let_it_be(:shared_project) do
create(:project, :private, name: 'D', path: 'D') create(:project, :private, name: 'D', path: 'D')
end end
...@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
describe 'filter by group name' do
let(:params) { { name: group.name, search_namespaces: true } }
it { is_expected.to eq([public_project, internal_project]) }
end
describe 'filter by archived' do describe 'filter by archived' do
let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') } let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') }
......
...@@ -22,12 +22,22 @@ describe OptionallySearch do ...@@ -22,12 +22,22 @@ describe OptionallySearch do
it 'delegates to the search method' do it 'delegates to the search method' do
expect(model) expect(model)
.to receive(:search) .to receive(:search)
.with('foo') .with('foo', {})
model.optionally_search('foo') model.optionally_search('foo')
end end
end end
context 'when an option is provided' do
it 'delegates to the search method' do
expect(model)
.to receive(:search)
.with('foo', some_option: true)
model.optionally_search('foo', some_option: true)
end
end
context 'when no query is given' do context 'when no query is given' do
it 'returns the current relation' do it 'returns the current relation' do
expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation)
......
...@@ -1757,7 +1757,7 @@ describe Project do ...@@ -1757,7 +1757,7 @@ describe Project do
expect(described_class.search(project.path.upcase)).to eq([project]) expect(described_class.search(project.path.upcase)).to eq([project])
end end
context 'by full path' do context 'when include_namespace is true' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
...@@ -1767,11 +1767,11 @@ describe Project do ...@@ -1767,11 +1767,11 @@ describe Project do
end end
it 'returns projects that match the group path' do it 'returns projects that match the group path' do
expect(described_class.search(group.path)).to eq([project]) expect(described_class.search(group.path, include_namespace: true)).to eq([project])
end end
it 'returns projects that match the full path' do it 'returns projects that match the full path' do
expect(described_class.search(project.full_path)).to eq([project]) expect(described_class.search(project.full_path, include_namespace: true)).to eq([project])
end end
end end
...@@ -1781,11 +1781,11 @@ describe Project do ...@@ -1781,11 +1781,11 @@ describe Project do
end end
it 'returns no results when searching by group path' do it 'returns no results when searching by group path' do
expect(described_class.search(group.path)).to be_empty expect(described_class.search(group.path, include_namespace: true)).to be_empty
end end
it 'returns no results when searching by full path' do it 'returns no results when searching by full path' do
expect(described_class.search(project.full_path)).to be_empty expect(described_class.search(project.full_path, include_namespace: true)).to be_empty
end end
end end
end end
......
...@@ -362,6 +362,21 @@ describe API::Projects do ...@@ -362,6 +362,21 @@ describe API::Projects do
end end
end end
context 'and using search and search_namespaces is true' do
let(:group) { create(:group) }
let!(:project_in_group) { create(:project, group: group) }
before do
group.add_guest(user)
end
it_behaves_like 'projects response' do
let(:filter) { { search: group.name, search_namespaces: true } }
let(:current_user) { user }
let(:projects) { [project_in_group] }
end
end
context 'and using id_after' do context 'and using id_after' do
it_behaves_like 'projects response' do it_behaves_like 'projects response' do
let(:filter) { { id_after: project2.id } } let(:filter) { { id_after: project2.id } }
......
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