Commit c521b828 authored by Stan Hu's avatar Stan Hu

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
parents 177f9ca8 0853c234
...@@ -17,6 +17,14 @@ const tokenKeys = [ ...@@ -17,6 +17,14 @@ const tokenKeys = [
icon: 'cube', icon: 'cube',
tag: 'type', tag: 'type',
}, },
{
key: 'tag',
type: 'array',
param: 'name[]',
symbol: '~',
icon: 'tag',
tag: '~tag',
},
]; ];
const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys); const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
......
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();
}
}
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 { addClassIfElementExists } from '../lib/utils/dom_utils';
import DropdownUtils from './dropdown_utils'; import DropdownAjaxFilter from './dropdown_ajax_filter';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
export default class DropdownUser extends FilteredSearchDropdown { export default class DropdownUser extends DropdownAjaxFilter {
constructor(options = {}) { constructor(options = {}) {
const { tokenKeys } = options; super({
super(options); ...options,
this.config = { endpoint: '/autocomplete/users.json',
AjaxFilter: { symbol: '@',
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, });
searchKey: 'search', }
params: {
active: true, ajaxFilterConfig() {
group_id: this.getGroupId(), return {
project_id: this.getProjectId(), ...super.ajaxFilterConfig(),
current_user: true, params: {
}, active: true,
searchValueFunction: this.getSearchInput.bind(this), group_id: this.getGroupId(),
loadingTemplate: this.loadingTemplate, project_id: this.getProjectId(),
onLoadingFinished: () => { current_user: true,
this.hideCurrentUser(); },
}, onLoadingFinished: () => {
onError() { this.hideCurrentUser();
/* eslint-disable no-new */
new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */
},
}, },
}; };
this.tokenKeys = tokenKeys;
} }
hideCurrentUser() { hideCurrentUser() {
addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden'); 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() { getGroupId() {
return this.input.getAttribute('data-group-id'); return this.input.getAttribute('data-group-id');
} }
...@@ -56,27 +36,4 @@ export default class DropdownUser extends FilteredSearchDropdown { ...@@ -56,27 +36,4 @@ export default class DropdownUser extends FilteredSearchDropdown {
getProjectId() { getProjectId() {
return this.input.getAttribute('data-project-id'); 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();
}
} }
...@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint'; ...@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint';
import DropdownEmoji from './dropdown_emoji'; import DropdownEmoji from './dropdown_emoji';
import DropdownNonUser from './dropdown_non_user'; import DropdownNonUser from './dropdown_non_user';
import DropdownUser from './dropdown_user'; import DropdownUser from './dropdown_user';
import DropdownAjaxFilter from './dropdown_ajax_filter';
import NullDropdown from './null_dropdown'; import NullDropdown from './null_dropdown';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
...@@ -111,6 +112,15 @@ export default class FilteredSearchDropdownManager { ...@@ -111,6 +112,15 @@ export default class FilteredSearchDropdownManager {
gl: NullDropdown, gl: NullDropdown,
element: this.container.querySelector('#js-dropdown-admin-runner-type'), 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 => { supportedTokens.forEach(type => {
...@@ -146,6 +156,10 @@ export default class FilteredSearchDropdownManager { ...@@ -146,6 +156,10 @@ export default class FilteredSearchDropdownManager {
return endpoint; return endpoint;
} }
getRunnerTagsEndpoint() {
return `${this.baseEndpoint}/admin/runners/tag_list.json`;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
const { uppercaseTokenName = false, capitalizeTokenValue = false } = options; const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
......
# frozen_string_literal: true # frozen_string_literal: true
class Admin::RunnersController < Admin::ApplicationController class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: :index before_action :runner, except: [:index, :tag_list]
def index def index
finder = Admin::RunnersFinder.new(params: params) finder = Admin::RunnersFinder.new(params: params)
...@@ -48,6 +48,12 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -48,6 +48,12 @@ class Admin::RunnersController < Admin::ApplicationController
end end
end end
def tag_list
tags = Autocomplete::ActsAsTaggableOn::TagsFinder.new(params: params).execute
render json: ActsAsTaggableOn::TagSerializer.new.represent(tags)
end
private private
def runner def runner
......
...@@ -11,6 +11,7 @@ class Admin::RunnersFinder < UnionFinder ...@@ -11,6 +11,7 @@ class Admin::RunnersFinder < UnionFinder
search! search!
filter_by_status! filter_by_status!
filter_by_runner_type! filter_by_runner_type!
filter_by_tag_list!
sort! sort!
paginate! paginate!
...@@ -44,6 +45,14 @@ class Admin::RunnersFinder < UnionFinder ...@@ -44,6 +45,14 @@ class Admin::RunnersFinder < UnionFinder
filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES) filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES)
end 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! def sort!
@runners = @runners.order_by(sort_key) @runners = @runners.order_by(sort_key)
end end
......
# 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
# frozen_string_literal: true
class ActsAsTaggableOn::TagEntity < Grape::Entity
expose :id
expose :name
end
# frozen_string_literal: true
class ActsAsTaggableOn::TagSerializer < BaseSerializer
entity ActsAsTaggableOn::TagEntity
end
...@@ -92,6 +92,25 @@ ...@@ -92,6 +92,25 @@
= button_tag class: %w[btn btn-link] do = button_tag class: %w[btn btn-link] do
= runner_type.titleize = 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 = button_tag class: %w[clear-search hidden] do
= icon('times') = icon('times')
.filter-dropdown-container .filter-dropdown-container
......
---
title: Add a tag filter to the admin runners view
merge_request: 19740
author: Alexis Reigel
type: added
...@@ -120,6 +120,10 @@ namespace :admin do ...@@ -120,6 +120,10 @@ namespace :admin do
get :resume get :resume
get :pause get :pause
end end
collection do
get :tag_list, format: :json
end
end end
resources :jobs, only: :index do resources :jobs, only: :index do
......
# 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
...@@ -2053,6 +2053,7 @@ ActiveRecord::Schema.define(version: 20190220150130) do ...@@ -2053,6 +2053,7 @@ ActiveRecord::Schema.define(version: 20190220150130) do
t.string "name" t.string "name"
t.integer "taggings_count", default: 0 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", unique: true, using: :btree
t.index ["name"], name: "index_tags_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
end end
create_table "term_agreements", force: :cascade do |t| create_table "term_agreements", force: :cascade do |t|
......
...@@ -13,13 +13,15 @@ GET /runners ...@@ -13,13 +13,15 @@ GET /runners
GET /runners?scope=active GET /runners?scope=active
GET /runners?type=project_type GET /runners?type=project_type
GET /runners?status=active GET /runners?status=active
GET /runners?tag_list=tag1,tag2
``` ```
| Attribute | Type | Required | Description | | 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 | | `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` | | `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` | | `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" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners"
...@@ -62,13 +64,15 @@ GET /runners/all ...@@ -62,13 +64,15 @@ GET /runners/all
GET /runners/all?scope=online GET /runners/all?scope=online
GET /runners/all?type=project_type GET /runners/all?type=project_type
GET /runners/all?status=active GET /runners/all?status=active
GET /runners/all?tag_list=tag1,tag2
``` ```
| Attribute | Type | Required | Description | | 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 | | `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` | | `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` | | `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" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/all"
...@@ -347,14 +351,16 @@ GET /projects/:id/runners ...@@ -347,14 +351,16 @@ GET /projects/:id/runners
GET /projects/:id/runners?scope=active GET /projects/:id/runners?scope=active
GET /projects/:id/runners?type=project_type GET /projects/:id/runners?type=project_type
GET /projects/:id/runners?status=active GET /projects/:id/runners?status=active
GET /projects/:id/runners?tag_list=tag1,tag2
``` ```
| Attribute | Type | Required | Description | | 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 | | `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 | | `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` | | `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` | | `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" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/runners"
......
...@@ -17,6 +17,7 @@ module API ...@@ -17,6 +17,7 @@ module API
desc: 'The type of the runners to show' desc: 'The type of the runners to show'
optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES, optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
desc: 'The status of the runners to show' desc: 'The status of the runners to show'
optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
use :pagination use :pagination
end end
get do get do
...@@ -24,6 +25,7 @@ module API ...@@ -24,6 +25,7 @@ module API
runners = filter_runners(runners, params[:scope], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES) 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[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES)
runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES) 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 present paginate(runners), with: Entities::Runner
end end
...@@ -38,6 +40,7 @@ module API ...@@ -38,6 +40,7 @@ module API
desc: 'The type of the runners to show' desc: 'The type of the runners to show'
optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES, optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
desc: 'The status of the runners to show' desc: 'The status of the runners to show'
optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
use :pagination use :pagination
end end
get 'all' do get 'all' do
...@@ -47,6 +50,7 @@ module API ...@@ -47,6 +50,7 @@ module API
runners = filter_runners(runners, params[:scope]) runners = filter_runners(runners, params[:scope])
runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES) 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 = 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 present paginate(runners), with: Entities::Runner
end end
...@@ -139,6 +143,7 @@ module API ...@@ -139,6 +143,7 @@ module API
desc: 'The type of the runners to show' desc: 'The type of the runners to show'
optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES, optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
desc: 'The status of the runners to show' desc: 'The status of the runners to show'
optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
use :pagination use :pagination
end end
get ':id/runners' do get ':id/runners' do
...@@ -146,6 +151,7 @@ module API ...@@ -146,6 +151,7 @@ module API
runners = filter_runners(runners, params[:scope]) runners = filter_runners(runners, params[:scope])
runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES) 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 = 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 present paginate(runners), with: Entities::Runner
end end
......
...@@ -615,6 +615,9 @@ msgstr "" ...@@ -615,6 +615,9 @@ msgstr ""
msgid "An error occurred creating the new branch." msgid "An error occurred creating the new branch."
msgstr "" msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
msgid "An error occurred previewing the blob" msgid "An error occurred previewing the blob"
msgstr "" msgstr ""
...@@ -4943,6 +4946,9 @@ msgstr "" ...@@ -4943,6 +4946,9 @@ msgstr ""
msgid "No %{providerTitle} repositories available to import" msgid "No %{providerTitle} repositories available to import"
msgstr "" msgstr ""
msgid "No Tag"
msgstr ""
msgid "No activities found" msgid "No activities found"
msgstr "" msgstr ""
......
...@@ -141,6 +141,56 @@ describe "Admin Runners" do ...@@ -141,6 +141,56 @@ describe "Admin Runners" do
end end
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 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-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') create(:ci_runner, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37')
......
...@@ -37,6 +37,14 @@ describe Admin::RunnersFinder do ...@@ -37,6 +37,14 @@ describe Admin::RunnersFinder do
end end
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 'sort' do
context 'without sort param' do context 'without sort param' do
it 'sorts by created_at' do it 'sorts by created_at' do
......
# 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
...@@ -90,6 +90,17 @@ describe API::Runners do ...@@ -90,6 +90,17 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end 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 end
context 'unauthorized user' do context 'unauthorized user' do
...@@ -181,6 +192,17 @@ describe API::Runners do ...@@ -181,6 +192,17 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end 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 end
context 'without admin privileges' do context 'without admin privileges' do
...@@ -716,6 +738,17 @@ describe API::Runners do ...@@ -716,6 +738,17 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end 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 end
context 'authorized user without maintainer privileges' do context 'authorized user without maintainer privileges' do
......
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