summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexis Reigel <alexis.reigel.ext@siemens.com>2018-11-15 16:10:10 +0100
committerAlexis Reigel <alexis.reigel.ext@siemens.com>2019-02-27 20:19:49 +0100
commit2e05292562e71deeff9b76bd3c696eca2a65a491 (patch)
tree3cceb216c54d7c55376b53421d273147d03b06ba
parent315361e025f5e490631d611b0f43b1814d1b0edc (diff)
downloadgitlab-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.
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js68
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js3
-rw-r--r--app/controllers/admin/runners_controller.rb4
-rw-r--r--app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb45
-rw-r--r--app/serializers/acts_as_taggable_on/tag_entity.rb6
-rw-r--r--app/serializers/acts_as_taggable_on/tag_serializer.rb5
-rw-r--r--app/services/autocomplete_tags_service.rb17
-rw-r--r--app/views/admin/runners/index.html.haml3
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb66
-rw-r--r--spec/services/autocomplete_tags_service_spec.rb17
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