add tag filter to admin runners page

parent d3accd36
...@@ -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);
......
...@@ -111,6 +111,15 @@ export default class FilteredSearchDropdownManager { ...@@ -111,6 +111,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: DropdownNonUser,
extraArguments: {
endpoint: this.getRunnerTagsEndpoint(),
symbol: '~',
},
element: this.container.querySelector('#js-dropdown-runner-tag'),
},
}; };
supportedTokens.forEach(type => { supportedTokens.forEach(type => {
...@@ -146,6 +155,10 @@ export default class FilteredSearchDropdownManager { ...@@ -146,6 +155,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,10 @@ def pause ...@@ -48,6 +48,10 @@ def pause
end end
end end
def tag_list
render json: AutocompleteTagsService.new(Ci::Runner).run
end
private private
def runner def runner
......
...@@ -11,6 +11,7 @@ def execute ...@@ -11,6 +11,7 @@ def execute
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 @@ def filter_by_runner_type! ...@@ -44,6 +45,14 @@ def filter_by_runner_type!
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
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
...@@ -92,6 +92,24 @@ ...@@ -92,6 +92,24 @@
= 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
{{title}}
= 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
......
...@@ -120,6 +120,10 @@ ...@@ -120,6 +120,10 @@
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
......
...@@ -4943,6 +4943,9 @@ msgstr "" ...@@ -4943,6 +4943,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 @@ ...@@ -141,6 +141,56 @@
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 @@ ...@@ -37,6 +37,14 @@
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
......
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
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