diff options
author | Alexis Reigel <alexis.reigel.ext@siemens.com> | 2018-11-15 16:10:10 +0100 |
---|---|---|
committer | Alexis Reigel <alexis.reigel.ext@siemens.com> | 2019-02-27 20:19:49 +0100 |
commit | 2e05292562e71deeff9b76bd3c696eca2a65a491 (patch) | |
tree | 3cceb216c54d7c55376b53421d273147d03b06ba | |
parent | 315361e025f5e490631d611b0f43b1814d1b0edc (diff) | |
download | gitlab-ce-2e05292562e71deeff9b76bd3c696eca2a65a491.tar.gz |
use lazy ajax filter dropdown for runner tags
the potential number of available runner tags is too large to load it
statically to a dropdown. we use the same lazy loaded dropdown as is
used for the users dropdown already.
11 files changed, 200 insertions, 37 deletions
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js new file mode 100644 index 00000000000..b27bb63c220 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js @@ -0,0 +1,68 @@ +import createFlash from '../flash'; +import AjaxFilter from '../droplab/plugins/ajax_filter'; +import FilteredSearchDropdown from './filtered_search_dropdown'; +import DropdownUtils from './dropdown_utils'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; +import { __ } from '~/locale'; + +export default class DropdownAjaxFilter extends FilteredSearchDropdown { + constructor(options = {}) { + const { tokenKeys, endpoint, symbol } = options; + + super(options); + + this.tokenKeys = tokenKeys; + this.endpoint = endpoint; + this.symbol = symbol; + + this.config = { + AjaxFilter: this.ajaxFilterConfig(), + }; + } + + ajaxFilterConfig() { + return { + endpoint: `${gon.relative_url_root || ''}${this.endpoint}`, + searchKey: 'search', + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + onError() { + createFlash(__('An error occurred fetching the dropdown data.')); + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, selected => + selected.querySelector('.dropdown-light-content').innerText.trim(), + ); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getSearchInput() { + const query = DropdownUtils.getSearchInput(this.input); + const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); + + let value = lastToken || ''; + + if (value[0] === this.symbol) { + value = value.slice(1); + } + + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === "'") { + value = value.slice(1); + } + + return value; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); + } +} diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 90293d9619a..57847d4ad9f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint'; import DropdownEmoji from './dropdown_emoji'; import DropdownNonUser from './dropdown_non_user'; import DropdownUser from './dropdown_user'; +import DropdownAjaxFilter from './dropdown_ajax_filter'; import NullDropdown from './null_dropdown'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; @@ -113,7 +114,7 @@ export default class FilteredSearchDropdownManager { }, tag: { reference: null, - gl: DropdownNonUser, + gl: DropdownAjaxFilter, extraArguments: { endpoint: this.getRunnerTagsEndpoint(), symbol: '~', diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 78edd325f1d..dd6b3c98496 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -49,7 +49,9 @@ class Admin::RunnersController < Admin::ApplicationController end def tag_list - render json: AutocompleteTagsService.new(Ci::Runner).run + tags = Autocomplete::ActsAsTaggableOn::TagsFinder.new(taggable_type: Ci::Runner, params: params).execute + + render json: ActsAsTaggableOn::TagSerializer.new.represent(tags) end private diff --git a/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb b/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb new file mode 100644 index 00000000000..65ae90c08b4 --- /dev/null +++ b/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Autocomplete + module ActsAsTaggableOn + class TagsFinder + LIMIT = 20 + + def initialize(taggable_type:, params:) + @taggable_type = taggable_type + @params = params + end + + def execute + @tags = @taggable_type.all_tags + + search! + limit! + + @tags + end + + def search! + search = @params[:search] + + return unless search + + if search.empty? + @tags = @taggable_type.none + return + end + + @tags = + if search.length >= Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING + @tags.named_like(search) + else + @tags.named(search) + end + end + + def limit! + @tags = @tags.limit(LIMIT) # rubocop: disable CodeReuse/ActiveRecord + end + end + end +end diff --git a/app/serializers/acts_as_taggable_on/tag_entity.rb b/app/serializers/acts_as_taggable_on/tag_entity.rb new file mode 100644 index 00000000000..d4e4b69f8fa --- /dev/null +++ b/app/serializers/acts_as_taggable_on/tag_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ActsAsTaggableOn::TagEntity < Grape::Entity + expose :id + expose :name +end diff --git a/app/serializers/acts_as_taggable_on/tag_serializer.rb b/app/serializers/acts_as_taggable_on/tag_serializer.rb new file mode 100644 index 00000000000..87f53606aa1 --- /dev/null +++ b/app/serializers/acts_as_taggable_on/tag_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ActsAsTaggableOn::TagSerializer < BaseSerializer + entity ActsAsTaggableOn::TagEntity +end diff --git a/app/services/autocomplete_tags_service.rb b/app/services/autocomplete_tags_service.rb deleted file mode 100644 index ccd0419e233..00000000000 --- a/app/services/autocomplete_tags_service.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class AutocompleteTagsService - def initialize(taggable_type) - @taggable_type = taggable_type - end - - # rubocop: disable CodeReuse/ActiveRecord - def run - @taggable_type - .all_tags - .pluck(:id, :name).map do |id, name| - { id: id, title: name } - end - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index f0a0a1897c7..2e23b748edb 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -108,7 +108,8 @@ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.js-data-value - {{title}} + %span.dropdown-light-content + {{name}} = button_tag class: %w[clear-search hidden] do = icon('times') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3cd6d9a88ce..507f3d0e8b4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -615,6 +615,9 @@ msgstr "" msgid "An error occurred creating the new branch." msgstr "" +msgid "An error occurred fetching the dropdown data." +msgstr "" + msgid "An error occurred previewing the blob" msgstr "" diff --git a/spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb b/spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb new file mode 100644 index 00000000000..9d1fac20362 --- /dev/null +++ b/spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::ActsAsTaggableOn::TagsFinder do + describe '#execute' do + context 'with empty params' do + it 'returns all tags' do + create :ci_runner, tag_list: ['tag1'] + create :ci_runner, tag_list: ['tag2'] + + tags = described_class.new(taggable_type: Ci::Runner, params: {}).execute.map(&:name) + + expect(tags).to match_array %w(tag1 tag2) + end + end + + context 'filter by search' do + context 'with an empty search term' do + it 'returns an empty collection' do + create :ci_runner, tag_list: ['tag1'] + create :ci_runner, tag_list: ['tag2'] + + tags = described_class.new(taggable_type: Ci::Runner, params: { search: '' }).execute.map(&:name) + + expect(tags).to be_empty + end + end + + context 'with a search containing 2 characters' do + it 'returns the tag that strictly matches the search term' do + create :ci_runner, tag_list: ['t1'] + create :ci_runner, tag_list: ['t11'] + + tags = described_class.new(taggable_type: Ci::Runner, params: { search: 't1' }).execute.map(&:name) + + expect(tags).to match_array ['t1'] + end + end + + context 'with a search containing 3 characters' do + it 'returns the tag that partially matches the search term' do + create :ci_runner, tag_list: ['tag1'] + create :ci_runner, tag_list: ['tag11'] + + tags = described_class.new(taggable_type: Ci::Runner, params: { search: 'ag1' }).execute.map(&:name) + + expect(tags).to match_array %w(tag1 tag11) + end + end + end + + context 'limit' do + it 'limits the result set by the limit constant' do + stub_const("#{described_class}::LIMIT", 1) + + create :ci_runner, tag_list: ['tag1'] + create :ci_runner, tag_list: ['tag2'] + + tags = described_class.new(taggable_type: Ci::Runner, params: { search: 'tag' }).execute + + expect(tags.count).to eq 1 + end + end + end +end diff --git a/spec/services/autocomplete_tags_service_spec.rb b/spec/services/autocomplete_tags_service_spec.rb deleted file mode 100644 index ff7128baa24..00000000000 --- a/spec/services/autocomplete_tags_service_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rails_helper' - -RSpec.describe AutocompleteTagsService do - describe '#run' do - it 'returns a hash of all tags' do - create(:ci_runner, tag_list: %w[tag1 tag2]) - create(:ci_runner, tag_list: %w[tag1 tag3]) - create(:project, tag_list: %w[tag4]) - - expect(described_class.new(Ci::Runner).run).to match_array [ - { id: kind_of(Integer), title: 'tag1' }, - { id: kind_of(Integer), title: 'tag2' }, - { id: kind_of(Integer), title: 'tag3' } - ] - end - end -end |