Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js14
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js115
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js77
-rw-r--r--app/assets/javascripts/filtered_search/null_dropdown.js9
-rw-r--r--app/assets/javascripts/pages/admin/runners/index.js10
-rw-r--r--app/assets/javascripts/pages/constants.js1
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js2
-rw-r--r--app/controllers/admin/runners_controller.rb9
-rw-r--r--app/finders/admin/runners_finder.rb53
-rw-r--r--app/helpers/sorting_helper.rb11
-rw-r--r--app/models/ci/runner.rb11
-rw-r--r--app/views/admin/runners/_runner.html.haml65
-rw-r--r--app/views/admin/runners/_runner_table_cell.html.haml4
-rw-r--r--app/views/admin/runners/_sort_dropdown.html.haml11
-rw-r--r--app/views/admin/runners/index.html.haml112
-rw-r--r--changelogs/unreleased/feature-runner-state-filter-for-admin-view.yml5
-rw-r--r--doc/api/runners.md4
-rw-r--r--lib/api/runners.rb17
-rw-r--r--locale/gitlab.pot51
-rw-r--r--spec/features/admin/admin_runners_spec.rb111
-rw-r--r--spec/finders/admin/runners_finder_spec.rb65
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js6
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js4
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js4
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js4
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js68
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js4
-rw-r--r--spec/models/ci/runner_spec.rb13
33 files changed, 629 insertions, 247 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')
- &nbsp;
- - 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')
diff --git a/changelogs/unreleased/feature-runner-state-filter-for-admin-view.yml b/changelogs/unreleased/feature-runner-state-filter-for-admin-view.yml
new file mode 100644
index 00000000000..b8112bd0813
--- /dev/null
+++ b/changelogs/unreleased/feature-runner-state-filter-for-admin-view.yml
@@ -0,0 +1,5 @@
+---
+title: Add a filter bar to the admin runners view and add a state filter
+merge_request: 19625
+author: Alexis Reigel
+type: added
diff --git a/doc/api/runners.md b/doc/api/runners.md
index ac814bbf19a..66476e7db64 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -15,7 +15,7 @@ GET /runners?scope=active
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
+| `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners"
@@ -60,7 +60,7 @@ GET /runners/all?scope=online
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
+| `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`, `offline`; showing all runners if none provided |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/all"
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index f35cf521e0a..30abd0b63e9 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -9,12 +9,12 @@ module API
success Entities::Runner
end
params do
- optional :scope, type: String, values: %w[active paused online],
+ optional :scope, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
desc: 'The scope of specific runners to show'
use :pagination
end
get do
- runners = filter_runners(current_user.ci_owned_runners, params[:scope], without: %w(specific shared))
+ runners = filter_runners(current_user.ci_owned_runners, params[:scope], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
present paginate(runners), with: Entities::Runner
end
@@ -22,7 +22,7 @@ module API
success Entities::Runner
end
params do
- optional :scope, type: String, values: %w[active paused online specific shared],
+ optional :scope, type: String, values: Ci::Runner::AVAILABLE_SCOPES,
desc: 'The scope of specific runners to show'
use :pagination
end
@@ -114,7 +114,7 @@ module API
success Entities::Runner
end
params do
- optional :scope, type: String, values: %w[active paused online specific shared],
+ optional :scope, type: String, values: Ci::Runner::AVAILABLE_SCOPES,
desc: 'The scope of specific runners to show'
use :pagination
end
@@ -160,15 +160,10 @@ module API
end
helpers do
- def filter_runners(runners, scope, options = {})
+ def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES)
return runners unless scope.present?
- available_scopes = ::Ci::Runner::AVAILABLE_SCOPES
- if options[:without]
- available_scopes = available_scopes - options[:without]
- end
-
- if (available_scopes & [scope]).empty?
+ unless allowed_scopes.include?(scope)
render_api_error!('Scope contains invalid value', 400)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7b6c15abd4f..db372811db3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3463,6 +3463,9 @@ msgstr ""
msgid "Last commit"
msgstr ""
+msgid "Last contact"
+msgstr ""
+
msgid "Last edited %{date}"
msgstr ""
@@ -3977,6 +3980,9 @@ msgstr ""
msgid "No repository"
msgstr ""
+msgid "No runners found"
+msgstr ""
+
msgid "No schedules"
msgstr ""
@@ -4438,6 +4444,9 @@ msgstr ""
msgid "Preferences|Navigation theme"
msgstr ""
+msgid "Press Enter or click to search"
+msgstr ""
+
msgid "Preview"
msgstr ""
@@ -4909,6 +4918,9 @@ msgstr ""
msgid "Real-time features"
msgstr ""
+msgid "Recent searches"
+msgstr ""
+
msgid "Reference:"
msgstr ""
@@ -5111,9 +5123,24 @@ msgstr ""
msgid "Run untagged jobs"
msgstr ""
+msgid "Runner cannot be assigned to other projects"
+msgstr ""
+
+msgid "Runner runs jobs from all unassigned projects"
+msgstr ""
+
+msgid "Runner runs jobs from all unassigned projects in its group"
+msgstr ""
+
+msgid "Runner runs jobs from assigned projects"
+msgstr ""
+
msgid "Runner token"
msgstr ""
+msgid "Runner will not receive any new jobs"
+msgstr ""
+
msgid "Runners"
msgstr ""
@@ -5123,6 +5150,12 @@ msgstr ""
msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
+msgid "Runners can be placed on separate users, servers, even on your local machine."
+msgstr ""
+
+msgid "Runners currently online: %{active_runners_count}"
+msgstr ""
+
msgid "Runners page"
msgstr ""
@@ -5195,6 +5228,9 @@ msgstr ""
msgid "Search milestones"
msgstr ""
+msgid "Search or filter results..."
+msgstr ""
+
msgid "Search or jump to…"
msgstr ""
@@ -5473,6 +5509,9 @@ msgstr ""
msgid "SortOptions|Largest repository"
msgstr ""
+msgid "SortOptions|Last Contact"
+msgstr ""
+
msgid "SortOptions|Last created"
msgstr ""
@@ -6346,6 +6385,9 @@ msgstr ""
msgid "Twitter"
msgstr ""
+msgid "Type"
+msgstr ""
+
msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
@@ -6484,6 +6526,9 @@ msgstr ""
msgid "Verified"
msgstr ""
+msgid "Version"
+msgstr ""
+
msgid "View file @ "
msgstr ""
@@ -6748,6 +6793,9 @@ msgstr ""
msgid "You can only edit files when you are on a branch"
msgstr ""
+msgid "You can reset runners registration token by pressing a button below."
+msgstr ""
+
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
@@ -7127,6 +7175,9 @@ msgstr ""
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
msgstr ""
+msgid "n/a"
+msgstr ""
+
msgid "new merge request"
msgstr ""
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 5623e47eadf..026dea8d22c 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
describe "Admin Runners" do
include StubENV
+ include FilteredSearchHelpers
+ include SortingHelper
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
@@ -12,40 +14,109 @@ describe "Admin Runners" do
let(:pipeline) { create(:ci_pipeline) }
context "when there are runners" do
- before do
- runner = FactoryBot.create(:ci_runner, contacted_at: Time.now)
- FactoryBot.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
+ it 'has all necessary texts' do
+ runner = create(:ci_runner, contacted_at: Time.now)
+ create(:ci_build, pipeline: pipeline, runner_id: runner.id)
visit admin_runners_path
- end
- it 'has all necessary texts' do
expect(page).to have_text "Setup a shared Runner manually"
expect(page).to have_text "Runners currently online: 1"
end
- describe 'search' do
+ describe 'search', :js do
before do
- FactoryBot.create :ci_runner, description: 'runner-foo'
- FactoryBot.create :ci_runner, description: 'runner-bar'
+ create(:ci_runner, description: 'runner-foo')
+ create(:ci_runner, description: 'runner-bar')
+
+ visit admin_runners_path
end
it 'shows correct runner when description matches' do
- search_form = find('#runners-search')
- search_form.fill_in 'search', with: 'runner-foo'
- search_form.click_button 'Search'
+ input_filtered_search_keys('runner-foo')
expect(page).to have_content("runner-foo")
expect(page).not_to have_content("runner-bar")
end
it 'shows no runner when description does not match' do
- search_form = find('#runners-search')
- search_form.fill_in 'search', with: 'runner-baz'
- search_form.click_button 'Search'
+ input_filtered_search_keys('runner-baz')
expect(page).to have_text 'No runners found'
end
end
+
+ describe 'filter by status', :js do
+ it 'shows correct runner when status matches' do
+ create(:ci_runner, description: 'runner-active', active: true)
+ create(:ci_runner, description: 'runner-paused', active: false)
+
+ visit admin_runners_path
+
+ expect(page).to have_content 'runner-active'
+ expect(page).to have_content 'runner-paused'
+
+ input_filtered_search_keys('status:active')
+ expect(page).to have_content 'runner-active'
+ expect(page).not_to have_content 'runner-paused'
+ end
+
+ it 'shows no runner when status does not match' do
+ create(:ci_runner, :online, description: 'runner-active', active: true)
+ create(:ci_runner, :online, description: 'runner-paused', active: false)
+
+ visit admin_runners_path
+
+ input_filtered_search_keys('status:offline')
+
+ expect(page).not_to have_content 'runner-active'
+ expect(page).not_to have_content 'runner-paused'
+
+ expect(page).to have_text 'No runners found'
+ end
+ end
+
+ it 'shows correct runner when status is selected and search term is entered', :js do
+ create(:ci_runner, description: 'runner-a-1', active: true)
+ create(:ci_runner, description: 'runner-a-2', active: false)
+ create(:ci_runner, description: 'runner-b-1', active: true)
+
+ visit admin_runners_path
+
+ input_filtered_search_keys('status:active')
+ 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('status:active 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
+
+ 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')
+
+ visit admin_runners_path
+
+ within '.runners-content .gl-responsive-table-row:nth-child(2)' do
+ expect(page).to have_content 'runner-2'
+ end
+
+ within '.runners-content .gl-responsive-table-row:nth-child(3)' do
+ expect(page).to have_content 'runner-1'
+ end
+
+ sorting_by 'Last Contact'
+
+ within '.runners-content .gl-responsive-table-row:nth-child(2)' do
+ expect(page).to have_content 'runner-1'
+ end
+
+ within '.runners-content .gl-responsive-table-row:nth-child(3)' do
+ expect(page).to have_content 'runner-2'
+ end
+ end
end
context "when there are no runners" do
@@ -76,7 +147,7 @@ describe "Admin Runners" do
context 'shared runner' do
it 'shows the label and does not show the project count' do
- runner = create :ci_runner, :instance
+ runner = create(:ci_runner, :instance)
visit admin_runners_path
@@ -89,8 +160,8 @@ describe "Admin Runners" do
context 'specific runner' do
it 'shows the label and the project count' do
- project = create :project
- runner = create :ci_runner, :project, projects: [project]
+ project = create(:project)
+ runner = create(:ci_runner, :project, projects: [project])
visit admin_runners_path
@@ -103,11 +174,11 @@ describe "Admin Runners" do
end
describe "Runner show page" do
- let(:runner) { FactoryBot.create :ci_runner }
+ let(:runner) { create(:ci_runner) }
before do
- @project1 = FactoryBot.create(:project)
- @project2 = FactoryBot.create(:project)
+ @project1 = create(:project)
+ @project2 = create(:project)
visit admin_runner_path(runner)
end
diff --git a/spec/finders/admin/runners_finder_spec.rb b/spec/finders/admin/runners_finder_spec.rb
new file mode 100644
index 00000000000..1e9793a5e0a
--- /dev/null
+++ b/spec/finders/admin/runners_finder_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::RunnersFinder do
+ describe '#execute' do
+ context 'with empty params' do
+ it 'returns all runners' do
+ runner1 = create :ci_runner, active: true
+ runner2 = create :ci_runner, active: false
+
+ expect(described_class.new(params: {}).execute).to match_array [runner1, runner2]
+ end
+ end
+
+ context 'filter by search term' do
+ it 'calls Ci::Runner.search' do
+ expect(Ci::Runner).to receive(:search).with('term').and_call_original
+
+ described_class.new(params: { search: 'term' }).execute
+ end
+ end
+
+ context 'filter by status' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:paused).and_call_original
+
+ described_class.new(params: { status_status: 'paused' }).execute
+ end
+ end
+
+ context 'sort' do
+ context 'without sort param' do
+ it 'sorts by created_at' do
+ runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
+ runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
+ runner3 = create :ci_runner, created_at: '2018-07-12 09:00'
+
+ expect(described_class.new(params: {}).execute).to eq [runner3, runner2, runner1]
+ end
+ end
+
+ context 'with sort param' do
+ it 'sorts by specified attribute' do
+ runner1 = create :ci_runner, contacted_at: 1.minute.ago
+ runner2 = create :ci_runner, contacted_at: 3.minutes.ago
+ runner3 = create :ci_runner, contacted_at: 2.minutes.ago
+
+ expect(described_class.new(params: { sort: 'contacted_asc' }).execute).to eq [runner2, runner3, runner1]
+ end
+ end
+ end
+
+ context 'paginate' do
+ it 'returns the runners for the specified page' do
+ stub_const('Admin::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 1)
+ runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
+ runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
+
+ expect(described_class.new(params: { page: 1 }).execute).to eq [runner2]
+ expect(described_class.new(params: { page: 2 }).execute).to eq [runner1]
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index d926663fac0..9d670afe206 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import eventHub from '~/filtered_search/event_hub';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
-import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
const createComponent = (propsData) => {
const Component = Vue.extend(RecentSearchesDropdownContent);
@@ -18,14 +18,14 @@ const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim();
describe('RecentSearchesDropdownContent', () => {
const propsDataWithoutItems = {
items: [],
- allowedKeys: FilteredSearchTokenKeys.getKeys(),
+ allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
};
const propsDataWithItems = {
items: [
'foo',
'author:@root label:~foo bar',
],
- allowedKeys: FilteredSearchTokenKeys.getKeys(),
+ allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
};
let vm;
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index c37a964975d..b48b1456eff 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -1,7 +1,7 @@
import DropdownUtils from '~/filtered_search/dropdown_utils';
import DropdownUser from '~/filtered_search/dropdown_user';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
-import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
+import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Dropdown User', () => {
describe('getSearchInput', () => {
@@ -14,7 +14,7 @@ describe('Dropdown User', () => {
spyOn(DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new DropdownUser({
- tokenKeys: FilteredSearchTokenKeys,
+ tokenKeys: IssuableFilteredTokenKeys,
});
});
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index 3d6dec19eca..8792e99d461 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -1,6 +1,6 @@
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
-import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => {
@@ -137,7 +137,7 @@ describe('Dropdown Utils', () => {
`);
input = document.getElementById('test');
- allowedKeys = FilteredSearchTokenKeys.getKeys();
+ allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
});
function config() {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 8fcee36beb8..a03d5a31b41 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,7 +1,7 @@
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import '~/lib/utils/common_utils';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
@@ -86,7 +86,7 @@ describe('Filtered Search Manager', function () {
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
isLocalStorageAvailable,
- allowedKeys: FilteredSearchTokenKeys.getKeys(),
+ allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
});
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
index 68158cf52e4..ab0ab72720e 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -1,26 +1,36 @@
import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
describe('Filtered Search Token Keys', () => {
- describe('get', () => {
- let tokenKeys;
+ const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ icon: 'pencil',
+ tag: '@author',
+ }];
+
+ const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+ }];
- beforeEach(() => {
- tokenKeys = FilteredSearchTokenKeys.get();
- });
+ describe('get', () => {
it('should return tokenKeys', () => {
- expect(tokenKeys !== null).toBe(true);
+ expect(new FilteredSearchTokenKeys().get() !== null).toBe(true);
});
it('should return tokenKeys as an array', () => {
- expect(tokenKeys instanceof Array).toBe(true);
+ expect(new FilteredSearchTokenKeys().get() instanceof Array).toBe(true);
});
});
describe('getKeys', () => {
it('should return keys', () => {
- const getKeys = FilteredSearchTokenKeys.getKeys();
- const keys = FilteredSearchTokenKeys.get().map(i => i.key);
+ const getKeys = new FilteredSearchTokenKeys(tokenKeys).getKeys();
+ const keys = new FilteredSearchTokenKeys(tokenKeys).get().map(i => i.key);
keys.forEach((key, i) => {
expect(key).toEqual(getKeys[i]);
@@ -29,88 +39,78 @@ describe('Filtered Search Token Keys', () => {
});
describe('getConditions', () => {
- let conditions;
-
- beforeEach(() => {
- conditions = FilteredSearchTokenKeys.getConditions();
- });
-
it('should return conditions', () => {
- expect(conditions !== null).toBe(true);
+ expect(new FilteredSearchTokenKeys().getConditions() !== null).toBe(true);
});
it('should return conditions as an array', () => {
- expect(conditions instanceof Array).toBe(true);
+ expect(new FilteredSearchTokenKeys().getConditions() instanceof Array).toBe(true);
});
});
describe('searchByKey', () => {
it('should return null when key not found', () => {
- const tokenKey = FilteredSearchTokenKeys.searchByKey('notakey');
+ const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchByKey('notakey');
expect(tokenKey === null).toBe(true);
});
it('should return tokenKey when found by key', () => {
- const tokenKeys = FilteredSearchTokenKeys.get();
- const result = FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
+ const result = new FilteredSearchTokenKeys(tokenKeys).searchByKey(tokenKeys[0].key);
expect(result).toEqual(tokenKeys[0]);
});
});
describe('searchBySymbol', () => {
it('should return null when symbol not found', () => {
- const tokenKey = FilteredSearchTokenKeys.searchBySymbol('notasymbol');
+ const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchBySymbol('notasymbol');
expect(tokenKey === null).toBe(true);
});
it('should return tokenKey when found by symbol', () => {
- const tokenKeys = FilteredSearchTokenKeys.get();
- const result = FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
+ const result = new FilteredSearchTokenKeys(tokenKeys).searchBySymbol(tokenKeys[0].symbol);
expect(result).toEqual(tokenKeys[0]);
});
});
describe('searchByKeyParam', () => {
it('should return null when key param not found', () => {
- const tokenKey = FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
+ const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam('notakeyparam');
expect(tokenKey === null).toBe(true);
});
it('should return tokenKey when found by key param', () => {
- const tokenKeys = FilteredSearchTokenKeys.get();
- const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ const result = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
expect(result).toEqual(tokenKeys[0]);
});
it('should return alternative tokenKey when found by key param', () => {
- const tokenKeys = FilteredSearchTokenKeys.getAlternatives();
- const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ const result = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
expect(result).toEqual(tokenKeys[0]);
});
});
describe('searchByConditionUrl', () => {
it('should return null when condition url not found', () => {
- const condition = FilteredSearchTokenKeys.searchByConditionUrl(null);
+ const condition = new FilteredSearchTokenKeys([], [], conditions).searchByConditionUrl(null);
expect(condition === null).toBe(true);
});
it('should return condition when found by url', () => {
- const conditions = FilteredSearchTokenKeys.getConditions();
- const result = FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
+ const result = new FilteredSearchTokenKeys([], [], conditions)
+ .searchByConditionUrl(conditions[0].url);
expect(result).toBe(conditions[0]);
});
});
describe('searchByConditionKeyValue', () => {
it('should return null when condition tokenKey and value not found', () => {
- const condition = FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
+ const condition = new FilteredSearchTokenKeys([], [], conditions)
+ .searchByConditionKeyValue(null, null);
expect(condition === null).toBe(true);
});
it('should return condition when found by tokenKey and value', () => {
- const conditions = FilteredSearchTokenKeys.getConditions();
- const result = FilteredSearchTokenKeys
+ const result = new FilteredSearchTokenKeys([], [], conditions)
.searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
expect(result).toEqual(conditions[0]);
});
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
index 465f5f79931..4f9f546cbb5 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
@@ -1,8 +1,8 @@
-import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
describe('Filtered Search Tokenizer', () => {
- const allowedKeys = FilteredSearchTokenKeys.getKeys();
+ const allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
describe('processTokens', () => {
it('returns for input containing only search value', () => {
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 953af2c4710..f1d0ed15d29 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -223,7 +223,7 @@ describe Ci::Runner do
subject { described_class.online }
before do
- @runner1 = create(:ci_runner, :instance, contacted_at: 1.year.ago)
+ @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago)
@runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago)
end
@@ -300,6 +300,17 @@ describe Ci::Runner do
end
end
+ describe '.offline' do
+ subject { described_class.offline }
+
+ before do
+ @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago)
+ @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago)
+ end
+
+ it { is_expected.to eq([@runner1])}
+ end
+
describe '#can_pick?' do
set(:pipeline) { create(:ci_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }