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.
This commit is contained in:
Alexis Reigel 2018-11-15 16:10:10 +01:00
parent 315361e025
commit 2e05292562
No known key found for this signature in database
GPG key ID: 55ADA7C7B683B329
11 changed files with 200 additions and 37 deletions

View file

@ -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();
}
}

View file

@ -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: '~',

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
# frozen_string_literal: true
class ActsAsTaggableOn::TagEntity < Grape::Entity
expose :id
expose :name
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class ActsAsTaggableOn::TagSerializer < BaseSerializer
entity ActsAsTaggableOn::TagEntity
end

View file

@ -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

View file

@ -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')

View file

@ -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 ""

View file

@ -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

View file

@ -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