diff options
author | Stan Hu <stanhu@gmail.com> | 2019-02-27 21:21:43 +0000 |
---|---|---|
committer | Stan Hu <stanhu@gmail.com> | 2019-02-27 21:21:43 +0000 |
commit | c521b82895da2f2229ccbc4c4f515df349a115df (patch) | |
tree | d1e726d12181c02f73b514daa7a7880d29b33b6c | |
parent | 177f9ca88b4d080e91b6c4ce2bba04d2fba95c07 (diff) | |
parent | 0853c234fc39dcfd70175d86990089712c462d58 (diff) | |
download | gitlab-ce-c521b82895da2f2229ccbc4c4f515df349a115df.tar.gz |
Merge branch 'feature/runner-tag-filter-for-admin-view' into 'master'
Feature: Runner tag filter for admin view
See merge request gitlab-org/gitlab-ce!19740
21 files changed, 419 insertions, 80 deletions
diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js index 934375023ba..691d165c585 100644 --- a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js @@ -17,6 +17,14 @@ const tokenKeys = [ icon: 'cube', tag: 'type', }, + { + key: 'tag', + type: 'array', + param: 'name[]', + symbol: '~', + icon: 'tag', + tag: '~tag', + }, ]; const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys); 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/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index d5027590bb7..f1e7be6bde1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -1,54 +1,34 @@ -import Flash from '../flash'; -import AjaxFilter from '../droplab/plugins/ajax_filter'; -import FilteredSearchDropdown from './filtered_search_dropdown'; import { addClassIfElementExists } from '../lib/utils/dom_utils'; -import DropdownUtils from './dropdown_utils'; -import FilteredSearchTokenizer from './filtered_search_tokenizer'; +import DropdownAjaxFilter from './dropdown_ajax_filter'; -export default class DropdownUser extends FilteredSearchDropdown { +export default class DropdownUser extends DropdownAjaxFilter { constructor(options = {}) { - const { tokenKeys } = options; - super(options); - this.config = { - AjaxFilter: { - endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, - searchKey: 'search', - params: { - active: true, - group_id: this.getGroupId(), - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput.bind(this), - loadingTemplate: this.loadingTemplate, - onLoadingFinished: () => { - this.hideCurrentUser(); - }, - onError() { - /* eslint-disable no-new */ - new Flash('An error occurred fetching the dropdown data.'); - /* eslint-enable no-new */ - }, + super({ + ...options, + endpoint: '/autocomplete/users.json', + symbol: '@', + }); + } + + ajaxFilterConfig() { + return { + ...super.ajaxFilterConfig(), + params: { + active: true, + group_id: this.getGroupId(), + project_id: this.getProjectId(), + current_user: true, + }, + onLoadingFinished: () => { + this.hideCurrentUser(); }, }; - this.tokenKeys = tokenKeys; } hideCurrentUser() { addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden'); } - 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); - } - getGroupId() { return this.input.getAttribute('data-group-id'); } @@ -56,27 +36,4 @@ export default class DropdownUser extends FilteredSearchDropdown { getProjectId() { return this.input.getAttribute('data-project-id'); } - - getSearchInput() { - const query = DropdownUtils.getSearchInput(this.input); - const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); - - let value = lastToken || ''; - - if (value[0] === '@') { - 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 4d05f46ed17..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'; @@ -111,6 +112,15 @@ export default class FilteredSearchDropdownManager { gl: NullDropdown, element: this.container.querySelector('#js-dropdown-admin-runner-type'), }, + tag: { + reference: null, + gl: DropdownAjaxFilter, + extraArguments: { + endpoint: this.getRunnerTagsEndpoint(), + symbol: '~', + }, + element: this.container.querySelector('#js-dropdown-runner-tag'), + }, }; supportedTokens.forEach(type => { @@ -146,6 +156,10 @@ export default class FilteredSearchDropdownManager { return endpoint; } + getRunnerTagsEndpoint() { + return `${this.baseEndpoint}/admin/runners/tag_list.json`; + } + static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { const { uppercaseTokenName = false, capitalizeTokenValue = false } = options; const input = FilteredSearchContainer.container.querySelector('.filtered-search'); diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 0b6ff491c66..8a00408001e 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Admin::RunnersController < Admin::ApplicationController - before_action :runner, except: :index + before_action :runner, except: [:index, :tag_list] def index finder = Admin::RunnersFinder.new(params: params) @@ -48,6 +48,12 @@ class Admin::RunnersController < Admin::ApplicationController end end + def tag_list + tags = Autocomplete::ActsAsTaggableOn::TagsFinder.new(params: params).execute + + render json: ActsAsTaggableOn::TagSerializer.new.represent(tags) + end + private def runner diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb index 8d936b8121c..b2799565f57 100644 --- a/app/finders/admin/runners_finder.rb +++ b/app/finders/admin/runners_finder.rb @@ -11,6 +11,7 @@ class Admin::RunnersFinder < UnionFinder search! filter_by_status! filter_by_runner_type! + filter_by_tag_list! sort! paginate! @@ -44,6 +45,14 @@ class Admin::RunnersFinder < UnionFinder filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES) end + def filter_by_tag_list! + tag_list = @params[:tag_name].presence + + if tag_list + @runners = @runners.tagged_with(tag_list) + end + end + def sort! @runners = @runners.order_by(sort_key) end 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..f38c187799c --- /dev/null +++ b/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Autocomplete + module ActsAsTaggableOn + class TagsFinder + LIMIT = 20 + + def initialize(params:) + @params = params + end + + def execute + tags = all_tags + tags = filter_by_name(tags) + limit(tags) + end + + private + + def all_tags + ::ActsAsTaggableOn::Tag.all + end + + def filter_by_name(tags) + return tags unless search + return tags.none if search.empty? + + 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 + + def search + @params[:search] + 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/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 81380587fd2..2e23b748edb 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -92,6 +92,25 @@ = button_tag class: %w[btn btn-link] do = runner_type.titleize + #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + - Ci::Runner::AVAILABLE_TYPES.each do |runner_type| + %li.filter-dropdown-item{ data: { value: runner_type } } + = button_tag class: %w[btn btn-link] do + = runner_type.titleize + + #js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } + %button.btn.btn-link + = _('No Tag') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value + %span.dropdown-light-content + {{name}} + = button_tag class: %w[clear-search hidden] do = icon('times') .filter-dropdown-container diff --git a/changelogs/unreleased/feature-runner-tag-filter-for-admin-view.yml b/changelogs/unreleased/feature-runner-tag-filter-for-admin-view.yml new file mode 100644 index 00000000000..86df4595e7b --- /dev/null +++ b/changelogs/unreleased/feature-runner-tag-filter-for-admin-view.yml @@ -0,0 +1,5 @@ +--- +title: Add a tag filter to the admin runners view +merge_request: 19740 +author: Alexis Reigel +type: added diff --git a/config/routes/admin.rb b/config/routes/admin.rb index af333bdc748..a01003b6039 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -120,6 +120,10 @@ namespace :admin do get :resume get :pause end + + collection do + get :tag_list, format: :json + end end resources :jobs, only: :index do diff --git a/db/migrate/20190206193120_add_index_to_tags.rb b/db/migrate/20190206193120_add_index_to_tags.rb new file mode 100644 index 00000000000..5257ebba003 --- /dev/null +++ b/db/migrate/20190206193120_add_index_to_tags.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexToTags < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_tags_on_name_trigram' + + disable_ddl_transaction! + + def up + add_concurrent_index :tags, :name, name: INDEX_NAME, using: :gin, opclasses: { name: :gin_trgm_ops } + end + + def down + remove_concurrent_index_by_name(:tags, INDEX_NAME) + end +end diff --git a/db/schema.rb b/db/schema.rb index a7a83475679..d4166e32112 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2053,6 +2053,7 @@ ActiveRecord::Schema.define(version: 20190220150130) do t.string "name" t.integer "taggings_count", default: 0 t.index ["name"], name: "index_tags_on_name", unique: true, using: :btree + t.index ["name"], name: "index_tags_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} end create_table "term_agreements", force: :cascade do |t| diff --git a/doc/api/runners.md b/doc/api/runners.md index 35c18649fec..7d7215e6b80 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -13,13 +13,15 @@ GET /runners GET /runners?scope=active GET /runners?type=project_type GET /runners?status=active +GET /runners?tag_list=tag1,tag2 ``` -| Attribute | Type | Required | Description | -|-----------|---------|----------|---------------------| -| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided | -| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | -| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | +| Attribute | Type | Required | Description | +|-------------|----------------|----------|---------------------| +| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided | +| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | +| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | +| `tag_list` | Array[String] | no | List of of the runner's tags | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners" @@ -62,13 +64,15 @@ GET /runners/all GET /runners/all?scope=online GET /runners/all?type=project_type GET /runners/all?status=active +GET /runners/all?tag_list=tag1,tag2 ``` -| Attribute | Type | Required | Description | -|-----------|---------|----------|---------------------| -| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`, `offline`; showing all runners if none provided | -| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | -| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | +| Attribute | Type | Required | Description | +|-------------|----------------|----------|---------------------| +| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`, `offline`; showing all runners if none provided | +| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | +| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | +| `tag_list` | Array[String] | no | List of of the runner's tags | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/all" @@ -347,14 +351,16 @@ GET /projects/:id/runners GET /projects/:id/runners?scope=active GET /projects/:id/runners?type=project_type GET /projects/:id/runners?status=active +GET /projects/:id/runners?tag_list=tag1,tag2 ``` -| Attribute | Type | Required | Description | -|-----------|----------------|----------|---------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided | -| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | -| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | +| Attribute | Type | Required | Description | +|------------|----------------|----------|---------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided | +| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | +| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | +| `tag_list` | Array[String] | no | List of of the runner's tags | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/runners" diff --git a/lib/api/runners.rb b/lib/api/runners.rb index f72b33605a7..f3fea463e7f 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -17,6 +17,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' + optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' use :pagination end get do @@ -24,6 +25,7 @@ module API runners = filter_runners(runners, params[:scope], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES) runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES) runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES) + runners = runners.tagged_with(params[:tag_list]) if params[:tag_list] present paginate(runners), with: Entities::Runner end @@ -38,6 +40,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' + optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' use :pagination end get 'all' do @@ -47,6 +50,7 @@ module API runners = filter_runners(runners, params[:scope]) runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES) runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES) + runners = runners.tagged_with(params[:tag_list]) if params[:tag_list] present paginate(runners), with: Entities::Runner end @@ -139,6 +143,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' + optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' use :pagination end get ':id/runners' do @@ -146,6 +151,7 @@ module API runners = filter_runners(runners, params[:scope]) runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES) runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES) + runners = runners.tagged_with(params[:tag_list]) if params[:tag_list] present paginate(runners), with: Entities::Runner end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 588dd766bdc..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 "" @@ -4943,6 +4946,9 @@ msgstr "" msgid "No %{providerTitle} repositories available to import" msgstr "" +msgid "No Tag" +msgstr "" + msgid "No activities found" msgstr "" diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index ed9c0ea9ac0..97b432a6751 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -141,6 +141,56 @@ describe "Admin Runners" do end end + describe 'filter by tag', :js do + it 'shows correct runner when tag matches' do + create :ci_runner, description: 'runner-blue', tag_list: ['blue'] + create :ci_runner, description: 'runner-red', tag_list: ['red'] + + visit admin_runners_path + + expect(page).to have_content 'runner-blue' + expect(page).to have_content 'runner-red' + + input_filtered_search_keys('tag:blue') + + expect(page).to have_content 'runner-blue' + expect(page).not_to have_content 'runner-red' + end + + it 'shows no runner when tag does not match' do + create :ci_runner, description: 'runner-blue', tag_list: ['blue'] + create :ci_runner, description: 'runner-red', tag_list: ['blue'] + + visit admin_runners_path + + input_filtered_search_keys('tag:red') + + expect(page).not_to have_content 'runner-blue' + expect(page).not_to have_content 'runner-blue' + expect(page).to have_text 'No runners found' + end + + it 'shows correct runner when tag is selected and search term is entered' do + create :ci_runner, description: 'runner-a-1', tag_list: ['blue'] + create :ci_runner, description: 'runner-a-2', tag_list: ['red'] + create :ci_runner, description: 'runner-b-1', tag_list: ['blue'] + + visit admin_runners_path + + input_filtered_search_keys('tag:blue') + + expect(page).to have_content 'runner-a-1' + expect(page).to have_content 'runner-b-1' + expect(page).not_to have_content 'runner-a-2' + + input_filtered_search_keys('tag:blue runner-a') + + expect(page).to have_content 'runner-a-1' + expect(page).not_to have_content 'runner-b-1' + expect(page).not_to have_content 'runner-a-2' + end + end + it 'sorts by last contact date', :js do create(:ci_runner, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37') create(:ci_runner, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37') diff --git a/spec/finders/admin/runners_finder_spec.rb b/spec/finders/admin/runners_finder_spec.rb index 0b2325cc7ca..94ccb398801 100644 --- a/spec/finders/admin/runners_finder_spec.rb +++ b/spec/finders/admin/runners_finder_spec.rb @@ -37,6 +37,14 @@ describe Admin::RunnersFinder do end end + context 'filter by tag_name' do + it 'calls the corresponding scope on Ci::Runner' do + expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original + + described_class.new(params: { tag_name: %w[tag1 tag2] }).execute + end + end + context 'sort' do context 'without sort param' do it 'sorts by created_at' do 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..79d2f9cdb45 --- /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 + tag1 = ActsAsTaggableOn::Tag.create!(name: 'tag1') + tag2 = ActsAsTaggableOn::Tag.create!(name: 'tag2') + + tags = described_class.new(params: {}).execute + + expect(tags).to match_array [tag1, tag2] + end + end + + context 'filter by search' do + context 'with an empty search term' do + it 'returns an empty collection' do + ActsAsTaggableOn::Tag.create!(name: 'tag1') + ActsAsTaggableOn::Tag.create!(name: 'tag2') + + tags = described_class.new(params: { search: '' }).execute + + 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 + tag1 = ActsAsTaggableOn::Tag.create!(name: 't1') + ActsAsTaggableOn::Tag.create!(name: 't11') + + tags = described_class.new(params: { search: 't1' }).execute + + expect(tags).to match_array [tag1] + end + end + + context 'with a search containing 3 characters' do + it 'returns the tag that partially matches the search term' do + tag1 = ActsAsTaggableOn::Tag.create!(name: 'tag1') + tag2 = ActsAsTaggableOn::Tag.create!(name: 'tag11') + + tags = described_class.new(params: { search: 'ag1' }).execute + + expect(tags).to match_array [tag1, tag2] + end + end + end + + context 'limit' do + it 'limits the result set by the limit constant' do + stub_const("#{described_class}::LIMIT", 1) + + ActsAsTaggableOn::Tag.create!(name: 'tag1') + ActsAsTaggableOn::Tag.create!(name: 'tag2') + + tags = described_class.new(params: { search: 'tag' }).execute + + expect(tags.count).to eq 1 + end + end + end +end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 5ca442bc448..5548e3fd01a 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -90,6 +90,17 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end + + it 'filters runners by tag_list' do + create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2]) + create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2']) + + get api('/runners?tag_list=tag1,tag2', user) + + expect(json_response).to match_array [ + a_hash_including('description' => 'Runner tagged with tag1 and tag2') + ] + end end context 'unauthorized user' do @@ -181,6 +192,17 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end + + it 'filters runners by tag_list' do + create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2]) + create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2']) + + get api('/runners/all?tag_list=tag1,tag2', admin) + + expect(json_response).to match_array [ + a_hash_including('description' => 'Runner tagged with tag1 and tag2') + ] + end end context 'without admin privileges' do @@ -716,6 +738,17 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end + + it 'filters runners by tag_list' do + create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2]) + create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2']) + + get api("/projects/#{project.id}/runners?tag_list=tag1,tag2", user) + + expect(json_response).to match_array [ + a_hash_including('description' => 'Runner tagged with tag1 and tag2') + ] + end end context 'authorized user without maintainer privileges' do |