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 {
this.groupId,
term,
{
search_namespaces: true,
with_issues_enabled: true,
with_shared: false,
include_subgroups: true,
......
......@@ -54,6 +54,7 @@ const projectSelect = () => {
this.groupId,
query.term,
{
search_namespaces: true,
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
......
......@@ -25,7 +25,7 @@ module Autocomplete
def execute
current_user
.projects_where_can_admin_issues
.optionally_search(search)
.optionally_search(search, include_namespace: true)
.excluding_project(project_id)
.eager_load_namespace_and_owner
.sorted_by_name_asc_limited(LIMIT)
......
......@@ -17,6 +17,7 @@
# tags: string[]
# personal: boolean
# search: string
# search_namespaces: boolean
# non_archived: boolean
# archived: 'only' or boolean
# min_access_level: integer
......@@ -171,7 +172,7 @@ class ProjectsFinder < UnionFinder
def by_search(items)
params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items
items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end
def by_deleted_status(items)
......
......@@ -12,8 +12,8 @@ module OptionallySearch
end
# Optionally limits a result set to those matching the given search query.
def optionally_search(query = nil)
query.present? ? search(query) : all
def optionally_search(query = nil, **options)
query.present? ? search(query, **options) : all
end
end
end
......@@ -591,9 +591,9 @@ class Project < ApplicationRecord
# case-insensitive.
#
# query - The search query as a String.
def search(query)
if Feature.enabled?(:project_search_by_full_path, default_enabled: true)
joins(:route).fuzzy_search(query, [Route.arel_table[:path], :name, :description])
def search(query, include_namespace: false)
if include_namespace && Feature.enabled?(:project_search_by_full_path, default_enabled: true)
joins(:route).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name], :description])
else
fuzzy_search(query, [:path, :name, :description])
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
| `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` |
| `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. |
| `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 |
......
......@@ -505,6 +505,7 @@ module API
finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
finder_params[:archived] = archived_param unless params[:archived].nil?
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[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
......
......@@ -63,6 +63,7 @@ module API
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
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 :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'
......
......@@ -100,6 +100,8 @@ describe 'Group issues page' do
find('.empty-state .js-lazy-loaded')
find('.new-project-item-link').click
find('.select2-input').set(group.name)
page.within('.select2-results') do
expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_issues_disabled.full_name)
......
......@@ -3,8 +3,8 @@
require 'spec_helper'
describe Autocomplete::MoveToProjectFinder do
let(:user) { create(:user) }
let(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:no_access_project) { create(:project) }
let(:guest_project) { create(:project) }
......@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do
expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a)
.to eq([wadus_project])
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
......@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
include AdminModeHelper
describe '#execute' do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let!(:private_project) do
let_it_be(:private_project) do
create(:project, :private, name: 'A', path: 'A')
end
let!(:internal_project) do
let_it_be(:internal_project) do
create(:project, :internal, group: group, name: 'B', path: 'B')
end
let!(:public_project) do
let_it_be(:public_project) do
create(:project, :public, group: group, name: 'C', path: 'C')
end
let!(:shared_project) do
let_it_be(:shared_project) do
create(:project, :private, name: 'D', path: 'D')
end
......@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) }
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
let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') }
......
......@@ -22,12 +22,22 @@ describe OptionallySearch do
it 'delegates to the search method' do
expect(model)
.to receive(:search)
.with('foo')
.with('foo', {})
model.optionally_search('foo')
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
it 'returns the current relation' do
expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation)
......
......@@ -1757,7 +1757,7 @@ describe Project do
expect(described_class.search(project.path.upcase)).to eq([project])
end
context 'by full path' do
context 'when include_namespace is true' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
......@@ -1767,11 +1767,11 @@ describe Project do
end
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
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
......@@ -1781,11 +1781,11 @@ describe Project do
end
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
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
......
......@@ -362,6 +362,21 @@ describe API::Projects do
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
it_behaves_like 'projects response' do
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