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
This commit is contained in:
commit
55c23a0935
|
@ -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;
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
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',
|
||||
}];
|
||||
|
||||
export default class FilteredSearchTokenKeys {
|
||||
static get() {
|
||||
return tokenKeys;
|
||||
constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) {
|
||||
this.tokenKeys = tokenKeys;
|
||||
this.alternativeTokenKeys = alternativeTokenKeys;
|
||||
this.conditions = conditions;
|
||||
|
||||
this.tokenKeysWithAlternative = this.tokenKeys.concat(this.alternativeTokenKeys);
|
||||
}
|
||||
|
||||
static getKeys() {
|
||||
return tokenKeys.map(i => i.key);
|
||||
get() {
|
||||
return this.tokenKeys;
|
||||
}
|
||||
|
||||
static getAlternatives() {
|
||||
return alternativeTokenKeys;
|
||||
getKeys() {
|
||||
return this.tokenKeys.map(i => i.key);
|
||||
}
|
||||
|
||||
static getConditions() {
|
||||
return conditions;
|
||||
getAlternatives() {
|
||||
return this.alternativeTokenKeys;
|
||||
}
|
||||
|
||||
static searchByKey(key) {
|
||||
return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
|
||||
getConditions() {
|
||||
return this.conditions;
|
||||
}
|
||||
|
||||
static searchBySymbol(symbol) {
|
||||
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
|
||||
searchByKey(key) {
|
||||
return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null;
|
||||
}
|
||||
|
||||
static searchByKeyParam(keyParam) {
|
||||
return tokenKeysWithAlternative.find((tokenKey) => {
|
||||
searchBySymbol(symbol) {
|
||||
return this.tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
|
@ -3,4 +3,5 @@
|
|||
export const FILTERED_SEARCH = {
|
||||
MERGE_REQUESTS: 'merge_requests',
|
||||
ISSUES: 'issues',
|
||||
ADMIN_RUNNERS: 'admin/runners',
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.table-section.section-10
|
||||
.table-mobile-header{ role: 'rowheader' }= label
|
||||
.table-mobile-content
|
||||
= yield
|
|
@ -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)
|
||||
|
|
@ -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'
|
||||
.bs-callout
|
||||
%p
|
||||
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
|
||||
|
||||
.float-right.light
|
||||
Runners currently online: #{@active_runners_cnt}
|
||||
|
||||
%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')
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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)
|
||||
visit admin_runners_path
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
tokenKeys = FilteredSearchTokenKeys.get();
|
||||
});
|
||||
const conditions = [{
|
||||
url: 'assignee_id=0',
|
||||
tokenKey: 'assignee',
|
||||
value: 'none',
|
||||
}];
|
||||
|
||||
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]);
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) }
|
||||
|
|
Loading…
Reference in New Issue