diff options
author | Grzegorz Bizon <grzegorz@gitlab.com> | 2018-09-14 12:03:44 +0300 |
---|---|---|
committer | Grzegorz Bizon <grzegorz@gitlab.com> | 2018-09-14 12:03:44 +0300 |
commit | 55c23a09352e8e6b4d0059ad47a76426bb0d7799 (patch) | |
tree | 2560a5fd450ebd2a5d19398786f6a9067984dd8e /app | |
parent | f076358491400008dfaa1c4b4c545acc66236706 (diff) | |
parent | f7ef78a7b5529a345d06b31df067f73c0b6c5833 (diff) |
Merge branch 'feature/runner-state-filter-for-admin-view' into 'master'
Feature: State filter for admin runners view
See merge request gitlab-org/gitlab-ce!19625
Diffstat (limited to 'app')
20 files changed, 352 insertions, 168 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 new file mode 100644 index 00000000000..1f9c3f41e52 --- /dev/null +++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js @@ -0,0 +1,14 @@ +import FilteredSearchTokenKeys from './filtered_search_token_keys'; + +const tokenKeys = [{ + key: 'status', + type: 'string', + param: 'status', + symbol: '', + icon: 'signal', + tag: 'status', +}]; + +const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys); + +export default AdminRunnersFilteredSearchTokenKeys; 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 296571606d6..a750647f8be 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 NullDropdown from './null_dropdown'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class FilteredSearchDropdownManager { @@ -90,6 +91,11 @@ export default class FilteredSearchDropdownManager { gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), }, + status: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-status'), + }, }; supportedTokens.forEach((type) => { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 81286c54c4c..d25f6f95b22 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -3,10 +3,10 @@ import { getParameterByName, getUrlParamsArray, } from '~/lib/utils/common_utils'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; -import FilteredSearchTokenKeys from './filtered_search_token_keys'; import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesService from './services/recent_searches_service'; @@ -23,7 +23,7 @@ export default class FilteredSearchManager { isGroup = false, isGroupAncestor = true, isGroupDecendent = false, - filteredSearchTokenKeys = FilteredSearchTokenKeys, + filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys, stateFiltersSelector = '.issues-state-filters', }) { this.isGroup = isGroup; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 087ef5cd6f2..5d131b396a0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -1,103 +1,38 @@ -const tokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - icon: 'pencil', - tag: '@author', -}, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - icon: 'user', - tag: '@assignee', -}, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - icon: 'clock-o', - tag: '%milestone', -}, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - icon: 'tag', - tag: '~label', -}]; - -if (gon.current_user_id) { - // Appending tokenkeys only logged-in - tokenKeys.push({ - key: 'my-reaction', - type: 'string', - param: 'emoji', - symbol: '', - icon: 'thumbs-up', - tag: 'emoji', - }); -} - -const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', -}]; - -const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); +export default class FilteredSearchTokenKeys { + constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) { + this.tokenKeys = tokenKeys; + this.alternativeTokenKeys = alternativeTokenKeys; + this.conditions = conditions; -const conditions = [{ - url: 'assignee_id=0', - tokenKey: 'assignee', - value: 'none', -}, { - url: 'milestone_title=No+Milestone', - tokenKey: 'milestone', - value: 'none', -}, { - url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', - value: 'upcoming', -}, { - url: 'milestone_title=%23started', - tokenKey: 'milestone', - value: 'started', -}, { - url: 'label_name[]=No+Label', - tokenKey: 'label', - value: 'none', -}]; + this.tokenKeysWithAlternative = this.tokenKeys.concat(this.alternativeTokenKeys); + } -export default class FilteredSearchTokenKeys { - static get() { - return tokenKeys; + get() { + return this.tokenKeys; } - static getKeys() { - return tokenKeys.map(i => i.key); + getKeys() { + return this.tokenKeys.map(i => i.key); } - static getAlternatives() { - return alternativeTokenKeys; + getAlternatives() { + return this.alternativeTokenKeys; } - static getConditions() { - return conditions; + getConditions() { + return this.conditions; } - static searchByKey(key) { - return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + searchByKey(key) { + return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null; } - static searchBySymbol(symbol) { - return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + searchBySymbol(symbol) { + return this.tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; } - static searchByKeyParam(keyParam) { - return tokenKeysWithAlternative.find((tokenKey) => { + searchByKeyParam(keyParam) { + return this.tokenKeysWithAlternative.find((tokenKey) => { let tokenKeyParam = tokenKey.key; // Replace hyphen with underscore to compare keyParam with tokenKeyParam @@ -112,12 +47,12 @@ export default class FilteredSearchTokenKeys { }) || null; } - static searchByConditionUrl(url) { - return conditions.find(condition => condition.url === url) || null; + searchByConditionUrl(url) { + return this.conditions.find(condition => condition.url === url) || null; } - static searchByConditionKeyValue(key, value) { - return conditions + searchByConditionKeyValue(key, value) { + return this.conditions .find(condition => condition.tokenKey === key && condition.value === value) || null; } } diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js new file mode 100644 index 00000000000..cce2c07dc3e --- /dev/null +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -0,0 +1,77 @@ +import FilteredSearchTokenKeys from './filtered_search_token_keys'; + +const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + icon: 'pencil', + tag: '@author', +}, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + icon: 'user', + tag: '@assignee', +}, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + icon: 'clock-o', + tag: '%milestone', +}, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + icon: 'tag', + tag: '~label', +}]; + +if (gon.current_user_id) { + // Appending tokenkeys only logged-in + tokenKeys.push({ + key: 'my-reaction', + type: 'string', + param: 'emoji', + symbol: '', + icon: 'thumbs-up', + tag: 'emoji', + }); +} + +const alternativeTokenKeys = [{ + key: 'label', + type: 'string', + param: 'name', + symbol: '~', +}]; + +const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', +}, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', +}, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', +}, { + url: 'milestone_title=%23started', + tokenKey: 'milestone', + value: 'started', +}, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', +}]; + +const IssuableFilteredSearchTokenKeys = + new FilteredSearchTokenKeys(tokenKeys, alternativeTokenKeys, conditions); + +export default IssuableFilteredSearchTokenKeys; diff --git a/app/assets/javascripts/filtered_search/null_dropdown.js b/app/assets/javascripts/filtered_search/null_dropdown.js new file mode 100644 index 00000000000..4cfce2a5beb --- /dev/null +++ b/app/assets/javascripts/filtered_search/null_dropdown.js @@ -0,0 +1,9 @@ +import FilteredSearchDropdown from './filtered_search_dropdown'; + +export default class NullDropdown extends FilteredSearchDropdown { + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [], this.config); + + super.renderContent(forceShowList); + } +} diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index.js new file mode 100644 index 00000000000..ce8fd18b6a2 --- /dev/null +++ b/app/assets/javascripts/pages/admin/runners/index.js @@ -0,0 +1,10 @@ +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys'; +import { FILTERED_SEARCH } from '~/pages/constants'; + +document.addEventListener('DOMContentLoaded', () => { + initFilteredSearch({ + page: FILTERED_SEARCH.ADMIN_RUNNERS, + filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys, + }); +}); diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js index 328b6541636..5e119454ce1 100644 --- a/app/assets/javascripts/pages/constants.js +++ b/app/assets/javascripts/pages/constants.js @@ -3,4 +3,5 @@ export const FILTERED_SEARCH = { MERGE_REQUESTS: 'merge_requests', ISSUES: 'issues', + ADMIN_RUNNERS: 'admin/runners', }; diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 914f804fdd3..736c6a62610 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,11 +1,13 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, isGroupDecendent: true, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); projectSelect(); }); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 1600faa3611..b798a254459 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,11 +1,13 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, isGroupDecendent: true, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); projectSelect(); }); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index 70fdb0ef40d..23edbdc5cad 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -4,12 +4,14 @@ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); new IssuableIndex(ISSUABLE_INDEX.ISSUE); diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index a7aa616319f..1fad0fb7297 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -2,12 +2,14 @@ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 090149fc1e2..2ac14ecd79c 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -3,11 +3,10 @@ class Admin::RunnersController < Admin::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def index - sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc } - @runners = Ci::Runner.order(sort) - @runners = @runners.search(params[:search]) if params[:search].present? - @runners = @runners.page(params[:page]).per(30) - @active_runners_cnt = Ci::Runner.online.count + finder = Admin::RunnersFinder.new(params: params) + @runners = finder.execute + @active_runners_count = Ci::Runner.online.count + @sort = finder.sort_key end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb new file mode 100644 index 00000000000..7adee486e33 --- /dev/null +++ b/app/finders/admin/runners_finder.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class Admin::RunnersFinder < UnionFinder + NUMBER_OF_RUNNERS_PER_PAGE = 30 + + def initialize(params:) + @params = params + end + + def execute + search! + filter_by_status! + sort! + paginate! + + @runners + end + + def sort_key + if @params[:sort] == 'contacted_asc' + 'contacted_asc' + else + 'created_date' + end + end + + private + + def search! + @runners = + if @params[:search].present? + Ci::Runner.search(@params[:search]) + else + Ci::Runner.all + end + end + + def filter_by_status! + status = @params[:status_status] + if status.present? && Ci::Runner::AVAILABLE_STATUSES.include?(status) + @runners = @runners.public_send(status) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def sort! + sort = sort_key == 'contacted_asc' ? { contacted_at: :asc } : { created_at: :desc } + @runners = @runners.order(sort) + end + + def paginate! + @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) + end +end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index a6e65d30eda..0c59bdd6abb 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -24,7 +24,8 @@ module SortingHelper sort_value_recently_updated => sort_title_recently_updated, sort_value_popularity => sort_title_popularity, sort_value_priority => sort_title_priority, - sort_value_upvotes => sort_title_upvotes + sort_value_upvotes => sort_title_upvotes, + sort_value_contacted_date => sort_title_contacted_date } end @@ -241,6 +242,10 @@ module SortingHelper s_('SortOptions|Most popular') end + def sort_title_contacted_date + s_('SortOptions|Last Contact') + end + # Values. def sort_value_access_level_asc 'access_level_asc' @@ -361,4 +366,8 @@ module SortingHelper def sort_value_upvotes 'upvotes_desc' end + + def sort_value_contacted_date + 'contacted_asc' + end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index f41955f43e7..45fd15a6211 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -11,7 +11,9 @@ module Ci RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes - AVAILABLE_SCOPES = %w[specific shared active paused online].freeze + AVAILABLE_TYPES = %w[specific shared].freeze + AVAILABLE_STATUSES = %w[active paused online offline].freeze + AVAILABLE_SCOPES = (AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze ignore_column :is_shared @@ -29,6 +31,13 @@ module Ci scope :active, -> { where(active: true) } scope :paused, -> { where(active: false) } scope :online, -> { where('contacted_at > ?', contact_time_deadline) } + # The following query using negation is cheaper than using `contacted_at <= ?` + # because there are less runners online than have been created. The + # resulting query is quickly finding online ones and then uses the regular + # indexed search and rejects the ones that are in the previous set. If we + # did `contacted_at <= ?` the query would effectively have to do a seq + # scan. + scope :offline, -> { where.not(id: online) } scope :ordered, -> { order(id: :desc) } # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb` diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 43937b01339..9c15226f0ec 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -1,5 +1,5 @@ -%tr{ id: dom_id(runner) } - %td +.gl-responsive-table-row{ id: dom_id(runner) } + = render layout: 'runner_table_cell', locals: { label: _('Type') } do - if runner.instance_type? %span.badge.badge-success shared - elsif runner.group_type? @@ -11,41 +11,50 @@ - unless runner.active? %span.badge.badge-danger paused - %td - = link_to admin_runner_path(runner) do - = runner.short_sha - %td + = render layout: 'runner_table_cell', locals: { label: _('Runner token') } do + = link_to runner.short_sha, admin_runner_path(runner) + + = render layout: 'runner_table_cell', locals: { label: _('Description') } do = runner.description - %td + + = render layout: 'runner_table_cell', locals: { label: _('Version') } do = runner.version - %td + + = render layout: 'runner_table_cell', locals: { label: _('IP Address') } do = runner.ip_address - %td + + = render layout: 'runner_table_cell', locals: { label: _('Projects') } do - if runner.instance_type? || runner.group_type? - n/a + = _('n/a') - else = runner.projects.count(:all) - %td - #{runner.builds.count(:all)} - %td + + = render layout: 'runner_table_cell', locals: { label: _('Jobs') } do + = runner.builds.count(:all) + + = render layout: 'runner_table_cell', locals: { label: _('Tags') } do - runner.tag_list.sort.each do |tag| %span.badge.badge-primary = tag - %td + + = render layout: 'runner_table_cell', locals: { label: _('Last contact') } do - if runner.contacted_at = time_ago_with_tooltip runner.contacted_at - else - Never - %td.admin-runner-btn-group-cell - .float-right.btn-group - = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do - = icon('pencil') - - - if runner.active? - = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-sm btn-default has-tooltip', title: 'Pause', ref: 'tooltip', aria: { label: 'Pause' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do - = icon('pause') - - else - = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-sm has-tooltip', title: 'Resume', ref: 'tooltip', aria: { label: 'Resume' }, data: { placement: 'top', container: 'body'} do - = icon('play') - = link_to [:admin, runner], method: :delete, class: 'btn btn-danger btn-sm has-tooltip', title: 'Remove', ref: 'tooltip', aria: { label: 'Remove' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do - = icon('remove') + = _('Never') + + .table-section.table-button-footer.section-10 + .btn-group.table-action-buttons + .btn-group + = link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do + = icon('pencil') + .btn-group + - if runner.active? + = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do + = icon('pause') + - else + = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do + = icon('play') + .btn-group + = link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do + = icon('remove') diff --git a/app/views/admin/runners/_runner_table_cell.html.haml b/app/views/admin/runners/_runner_table_cell.html.haml new file mode 100644 index 00000000000..78526ee6d23 --- /dev/null +++ b/app/views/admin/runners/_runner_table_cell.html.haml @@ -0,0 +1,4 @@ +.table-section.section-10 + .table-mobile-header{ role: 'rowheader' }= label + .table-mobile-content + = yield diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml new file mode 100644 index 00000000000..b201e6bf10e --- /dev/null +++ b/app/views/admin/runners/_sort_dropdown.html.haml @@ -0,0 +1,11 @@ +- sorted_by = sort_options_hash[@sort] + +.dropdown.inline.prepend-left-10 + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } + = sorted_by + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort + %li + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) + = sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date, label: true), sorted_by) + diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 9280ff4d478..4dc076c95c5 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,77 +1,107 @@ -- breadcrumb_title "Runners" +- breadcrumb_title _('Runners') - @no_container = true %div{ class: container_class } .bs-callout %p - A 'Runner' is a process which runs a job. - You can setup as many Runners as you need. + = (_"A 'Runner' is a process which runs a job. You can setup as many Runners as you need.") %br - Runners can be placed on separate users, servers, even on your local machine. + = _('Runners can be placed on separate users, servers, even on your local machine.') %br %div - %span Each Runner can be in one of the following states: + %span= _('Each Runner can be in one of the following states:') %ul %li %span.badge.badge-success shared - \- Runner runs jobs from all unassigned projects + \- + = _('Runner runs jobs from all unassigned projects') %li %span.badge.badge-success group - \- Runner runs jobs from all unassigned projects in its group + \- + = _('Runner runs jobs from all unassigned projects in its group') %li %span.badge.badge-info specific - \- Runner runs jobs from assigned projects + \- + = _('Runner runs jobs from assigned projects') %li %span.badge.badge-warning locked - \- Runner cannot be assigned to other projects + \- + = _('Runner cannot be assigned to other projects') %li %span.badge.badge-danger paused - \- Runner will not receive any new jobs + \- + = _('Runner will not receive any new jobs') .bs-callout.clearfix .float-left %p - You can reset runners registration token by pressing a button below. + = _('You can reset runners registration token by pressing a button below.') .prepend-top-10 - = button_to _("Reset runners registration token"), reset_runners_token_admin_application_settings_path, + = button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path, method: :put, class: 'btn btn-default', - data: { confirm: _("Are you sure you want to reset registration token?") } + data: { confirm: _('Are you sure you want to reset registration token?') } = render partial: 'ci/runner/how_to_setup_shared_runner', locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } - .append-bottom-20.clearfix - .float-left - = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do - .form-group - = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false - = submit_tag 'Search', class: 'btn' - - .float-right.light - Runners currently online: #{@active_runners_cnt} + .bs-callout + %p + = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count } - %br + .row-content-block.second-block + = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do + .filtered-search-wrapper + .filtered-search-box + = dropdown_tag(custom_icon('icon_history'), + options: { wrapper_class: 'filtered-search-history-dropdown-wrapper', + toggle_class: 'filtered-search-history-dropdown-toggle-button', + dropdown_class: 'filtered-search-history-dropdown', + content_class: 'filtered-search-history-dropdown-content', + title: _('Recent searches') }) do + .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } } + .filtered-search-box-input-container.droplab-dropdown + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } } + #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } + = button_tag class: %w[btn btn-link] do + = icon('search') + %span + = _('Press Enter or click to search') + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + = button_tag class: %w[btn btn-link] do + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %i.fa{ class: "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + - Ci::Runner::AVAILABLE_STATUSES.each do |status| + %li.filter-dropdown-item{ data: { value: status } } + = button_tag class: %w[btn btn-link] do + = status.titleize + = button_tag class: %w[clear-search hidden] do + = icon('times') + .filter-dropdown-container + = render 'sort_dropdown' - if @runners.any? - .runners-content + .runners-content.content-list .table-holder - %table.table - %thead - %tr - %th Type - %th Runner token - %th Description - %th Version - %th IP Address - %th Projects - %th Jobs - %th Tags - %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc')) - %th + .gl-responsive-table-row.table-row-header{ role: 'row' } + - [_('Type'), _('Runner token'), _('Description'), _('Version'), _('IP Address'), _('Projects'), _('Jobs'), _('Tags'), _('Last contact')].each do |label| + .table-section.section-10{ role: 'rowheader' }= label - - @runners.each do |runner| - = render "admin/runners/runner", runner: runner - = paginate @runners, theme: "gitlab" + - @runners.each do |runner| + = render 'admin/runners/runner', runner: runner + = paginate @runners, theme: 'gitlab' - else - .nothing-here-block No runners found + .nothing-here-block= _('No runners found') |