diff options
Diffstat (limited to 'app')
121 files changed, 778 insertions, 305 deletions
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 39d90a64aab..eb596b251c1 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -11,8 +11,10 @@ export default () => ({ endpoint: '', basePath: '', commit: null, + startVersion: null, diffFiles: [], mergeRequestDiffs: [], + mergeRequestDiff: null, diffLineCommentForms: {}, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, }); diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js index 20d1ebbe049..6860e24db6b 100644 --- a/app/assets/javascripts/diffs/store/modules/index.js +++ b/app/assets/javascripts/diffs/store/modules/index.js @@ -3,10 +3,10 @@ import * as getters from '../getters'; import mutations from '../mutations'; import createState from './diff_state'; -export default { +export default () => ({ namespaced: true, state: createState(), getters, actions, mutations, -}; +}); 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/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js index dd2019001db..446eb477efc 100644 --- a/app/assets/javascripts/mr_notes/stores/index.js +++ b/app/assets/javascripts/mr_notes/stores/index.js @@ -9,7 +9,7 @@ Vue.use(Vuex); export default new Vuex.Store({ modules: { page: mrPageModule, - notes: notesModule, - diffs: diffsModule, + notes: notesModule(), + diffs: diffsModule(), }, }); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 9a2ec15debd..68df63b8539 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -10,6 +10,7 @@ import service from '../services/notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; +import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; let eTagPoll; @@ -61,9 +62,11 @@ export const refetchDiscussionById = ({ commit, state }, { path, discussionId }) .catch(() => {}); }); -export const deleteNote = ({ commit }, note) => +export const deleteNote = ({ commit, dispatch }, note) => service.deleteNote(note.path).then(() => { commit(types.DELETE_NOTE, note); + + dispatch('updateMergeRequestWidget'); }); export const updateNote = ({ commit }, { endpoint, note }) => @@ -84,20 +87,22 @@ export const replyToDiscussion = ({ commit }, { endpoint, data }) => return res; }); -export const createNewNote = ({ commit }, { endpoint, data }) => +export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => service .createNewNote(endpoint, data) .then(res => res.json()) .then(res => { if (!res.errors) { commit(types.ADD_NEW_NOTE, res); + + dispatch('updateMergeRequestWidget'); } return res; }); export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => +export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => service .toggleResolveNote(endpoint, isResolved) .then(res => res.json()) @@ -105,6 +110,8 @@ export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; commit(mutationType, res); + + dispatch('updateMergeRequestWidget'); }); export const closeIssue = ({ commit, dispatch, state }) => { @@ -333,5 +340,9 @@ export const fetchDiscussionDiffLines = ({ commit }, discussion) => }); }); +export const updateMergeRequestWidget = () => { + mrWidgetEventHub.$emit('mr.discussion.updated'); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index 0f48b8880f4..f105b7d0d11 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -1,16 +1,8 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import module from './modules'; +import notesModule from './modules'; Vue.use(Vuex); export default () => - new Vuex.Store({ - state: module.state, - actions, - getters, - mutations, - }); + new Vuex.Store(notesModule()); diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index b4cb9267e0f..61dbb075586 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -2,7 +2,7 @@ import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; -export default { +export default () => ({ state: { discussions: [], targetNoteHash: null, @@ -24,4 +24,4 @@ export default { actions, getters, mutations, -}; +}); diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 2c04bfea122..16bb54be126 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -214,12 +214,7 @@ export default { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - const index = state.discussions.indexOf(discussion); - const discussionWithDiffLines = Object.assign({}, discussion, { - truncated_diff_lines: diffLines, - }); - - state.discussions.splice(index, 1, discussionWithDiffLines); + discussion.truncated_diff_lines = diffLines; }, }; 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/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 69a9132a2da..cc6e620f365 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,7 +1,4 @@ -import { - Vue, - mrWidgetOptions, -} from './dependencies'; +import { Vue, mrWidgetOptions } from './dependencies'; import Translate from '../vue_shared/translate'; Vue.use(Translate); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index dc6be025f11..b5eaaf054e7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -107,10 +107,14 @@ export default { created() { this.initPolling(); this.bindEventHubListeners(); + eventHub.$on('mr.discussion.updated', this.checkStatus); }, mounted() { this.handleMounted(); }, + beforeDestroy() { + eventHub.$off('mr.discussion.updated', this.checkStatus); + }, methods: { createService(store) { const endpoints = { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index cb29b5d4313..57539212e0c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -703,7 +703,8 @@ text-overflow: ellipsis; .user-status-emoji { - margin: 0 $gl-padding-8 0 $gl-padding-4; + margin-left: $gl-padding-4; + margin-right: 0; } } diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 090149fc1e2..911603bac17 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -1,15 +1,12 @@ class Admin::RunnersController < Admin::ApplicationController before_action :runner, except: :index - # 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 def show assign_builds_and_projects diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 0469e7e1e1f..78f7f6d4e23 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -22,7 +22,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController private def group_milestones - groups = GroupsFinder.new(current_user, all_available: true).execute + groups = GroupsFinder.new(current_user, all_available: false).execute DashboardGroupMilestone.build_collection(groups) end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 94e6efca487..a43d47797f1 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -96,6 +96,7 @@ class ProfilesController < Profiles::ApplicationController :location, :name, :public_email, + :commit_email, :skype, :twitter, :username, diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index b4fd09c06e5..eb0fad6cbb2 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -141,7 +141,8 @@ class Projects::ClustersController < Projects::ApplicationController :gcp_project_id, :zone, :num_nodes, - :machine_type + :machine_type, + :legacy_abac ]).merge( provider_type: :gcp, platform_type: :kubernetes diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb index b6ee49df99b..2cc8a978877 100644 --- a/app/finders/access_requests_finder.rb +++ b/app/finders/access_requests_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AccessRequestsFinder attr_accessor :source diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb index 6b805c432f6..e2b9b0b44c1 100644 --- a/app/finders/admin/projects_finder.rb +++ b/app/finders/admin/projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::ProjectsFinder attr_reader :params, :current_user diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb new file mode 100644 index 00000000000..3c2d7ee7d76 --- /dev/null +++ b/app/finders/admin/runners_finder.rb @@ -0,0 +1,52 @@ +# 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! + @runners = @runners.order_by(sort_key) + end + + def paginate! + @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) + end +end diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 8bb1366867c..970efa79dfb 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BranchesFinder def initialize(repository, params = {}) @repository = repository diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb index c13f98257bf..b40d6c41b71 100644 --- a/app/finders/clusters_finder.rb +++ b/app/finders/clusters_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ClustersFinder def initialize(project, user, scope) @project = project diff --git a/app/finders/concerns/created_at_filter.rb b/app/finders/concerns/created_at_filter.rb index ac9ac77732c..6b5863a5c53 100644 --- a/app/finders/concerns/created_at_filter.rb +++ b/app/finders/concerns/created_at_filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CreatedAtFilter def by_created_at(items) items = items.created_before(params[:created_before]) if params[:created_before].present? diff --git a/app/finders/concerns/custom_attributes_filter.rb b/app/finders/concerns/custom_attributes_filter.rb index 022a19b0a7f..825c3a6b5b7 100644 --- a/app/finders/concerns/custom_attributes_filter.rb +++ b/app/finders/concerns/custom_attributes_filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CustomAttributesFilter # rubocop: disable CodeReuse/ActiveRecord def by_custom_attributes(items) diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb index ce0ec214b8a..5290313585f 100644 --- a/app/finders/concerns/finder_methods.rb +++ b/app/finders/concerns/finder_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FinderMethods # rubocop: disable CodeReuse/ActiveRecord def find_by!(*args) diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb index f70a0b60864..e038636f0c4 100644 --- a/app/finders/concerns/finder_with_cross_project_access.rb +++ b/app/finders/concerns/finder_with_cross_project_access.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Module to prepend into finders to specify wether or not the finder requires # cross project access # diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb index 1447def0cdf..c1ef9dfefa7 100644 --- a/app/finders/contributed_projects_finder.rb +++ b/app/finders/contributed_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ContributedProjectsFinder < UnionFinder def initialize(user) @user = user diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb index 95af6214c7d..419be46fafe 100644 --- a/app/finders/environments_finder.rb +++ b/app/finders/environments_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EnvironmentsFinder attr_reader :project, :current_user, :params diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index a8d4f56a84c..fd7aeca0d8b 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EventsFinder prepend FinderMethods prepend FinderWithCrossProjectAccess diff --git a/app/finders/fork_projects_finder.rb b/app/finders/fork_projects_finder.rb index 8713d43136f..03ace7e8057 100644 --- a/app/finders/fork_projects_finder.rb +++ b/app/finders/fork_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ForkProjectsFinder < ProjectsFinder # rubocop: disable CodeReuse/ActiveRecord def initialize(project, params: {}, current_user: nil) diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 15060a99930..9d57d2d3bc9 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # GroupDescendantsFinder # # Used to find and filter all subgroups and projects of a passed parent group diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb index 8fde3a00af7..d2ad8a372b1 100644 --- a/app/finders/group_finder.rb +++ b/app/finders/group_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GroupFinder include Gitlab::Allowable diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index cab1ff8fb98..eebc67cfa9e 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GroupMembersFinder def initialize(group) @group = group diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 52a347fa598..4155b6af8da 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # GroupProjectsFinder # # Used to filter Projects by set of params diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 436abb332ff..a35a3ed6142 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # GroupsFinder # # Used to filter Groups by a set of params diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3308fd6d697..251a559878a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # IssuableFinder # # Used to filter Issues and MergeRequests collections by set of params diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index c347ef1ec19..770e0bfe1a3 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Finders::Issues class # # Used to filter Issues collections by set of params diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb index 47174980258..18cc6891ca4 100644 --- a/app/finders/joined_groups_finder.rb +++ b/app/finders/joined_groups_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JoinedGroupsFinder < UnionFinder def initialize(user) @user = user diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 17c55f8db50..08fc2968e77 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LabelsFinder < UnionFinder prepend FinderWithCrossProjectAccess include FinderMethods diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb index fad33f0eca2..196922709f7 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # LicenseTemplateFinder # # Used to find license templates, which may come from a variety of external diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 58e3606692d..48777838d60 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MembersFinder attr_reader :project, :current_user, :group diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index 66d4c4018f3..5f0589f6c8b 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MergeRequestTargetProjectFinder include FinderMethods diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index c6403f8f25d..b698a3c7b09 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Finders::MergeRequest class # # Used to filter MergeRequests collections by set of params diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 7dc2a5b0b66..47231ea80c7 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Search for milestones # # params - Hash diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 580984329a1..c67c2065440 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotesFinder FETCH_OVERLAP = 5.seconds diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 6b610ffc687..5beea92689f 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PersonalAccessTokensFinder attr_accessor :params diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb index 16e4e060786..20f5b221a89 100644 --- a/app/finders/personal_projects_finder.rb +++ b/app/finders/personal_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PersonalProjectsFinder < UnionFinder include Gitlab::Allowable diff --git a/app/finders/pipeline_schedules_finder.rb b/app/finders/pipeline_schedules_finder.rb index d131e1659c1..3beee608268 100644 --- a/app/finders/pipeline_schedules_finder.rb +++ b/app/finders/pipeline_schedules_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelineSchedulesFinder attr_reader :project, :pipeline_schedules diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index b03d730f975..3d0d3219a94 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelinesFinder attr_reader :project, :pipelines, :params, :current_user diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 0f2fdf317a4..c2404412006 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # ProjectsFinder # # Used to filter Projects by set of params diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index d4f2474197f..4fca4ec94f3 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RunnerJobsFinder attr_reader :runner, :params diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 32da3e8f404..715dffb972f 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Snippets Finder # # Used to filter Snippets collections by a set of params diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb index b474f0805dc..2ffd46245e9 100644 --- a/app/finders/tags_finder.rb +++ b/app/finders/tags_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TagsFinder def initialize(repository, params) @repository = repository diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb index 7cdc7a32acc..c92ee9ca9ac 100644 --- a/app/finders/template_finder.rb +++ b/app/finders/template_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TemplateFinder VENDORED_TEMPLATES = { dockerfiles: ::Gitlab::Template::DockerfileTemplate, diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 6481893a195..0d9f16fdce7 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # TodosFinder # # Used to filter Todos by set of params diff --git a/app/finders/union_finder.rb b/app/finders/union_finder.rb index 91b845871a8..798c3eba0f3 100644 --- a/app/finders/union_finder.rb +++ b/app/finders/union_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UnionFinder # rubocop: disable CodeReuse/ActiveRecord def find_union(segments, klass) diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index 7405d6d27d9..a4daf5b5841 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Get user activity feed for projects common for a user and a logged in user # # - current_user: The user viewing the events diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 46672301851..f2ad9b4bda5 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # UsersFinder # # Used to filter users by set of params diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb index 42fb8f99acc..2512ecbd255 100644 --- a/app/graphql/functions/base_function.rb +++ b/app/graphql/functions/base_function.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Functions class BaseFunction < GraphQL::Function end diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb index e5bf109b8d7..3104486faac 100644 --- a/app/graphql/functions/echo.rb +++ b/app/graphql/functions/echo.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Functions class Echo < BaseFunction argument :text, GraphQL::STRING_TYPE diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 8755a1a62e7..06d26309b5b 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GitlabSchema < GraphQL::Schema use BatchLoader::GraphQL use Gitlab::Graphql::Authorize diff --git a/app/graphql/mutations/concerns/mutations/resolves_project.rb b/app/graphql/mutations/concerns/mutations/resolves_project.rb index 0dd1f264a52..da9814e88b0 100644 --- a/app/graphql/mutations/concerns/mutations/resolves_project.rb +++ b/app/graphql/mutations/concerns/mutations/resolves_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mutations module ResolvesProject extend ActiveSupport::Concern diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb index 2149e72e2df..54f01c99d78 100644 --- a/app/graphql/mutations/merge_requests/base.rb +++ b/app/graphql/mutations/merge_requests/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mutations module MergeRequests class Base < BaseMutation diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 89b7f9dad6f..459933af9d3 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class BaseResolver < GraphQL::Schema::Resolver end diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb index 9ec45378d8e..8fd26d85994 100644 --- a/app/graphql/resolvers/concerns/resolves_pipelines.rb +++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ResolvesPipelines extend ActiveSupport::Concern diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index 4eb28aaed6c..8d3da33e8d2 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers module FullPathResolver extend ActiveSupport::Concern diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb index 00b51ee1381..b371f1335f8 100644 --- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb +++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class MergeRequestPipelinesResolver < BaseResolver include ::ResolvesPipelines diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index 6b7d0ce6e24..b87c95217f7 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class MergeRequestResolver < BaseResolver argument :iid, GraphQL::ID_TYPE, diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb index 7f175a3b26c..86094c46c2a 100644 --- a/app/graphql/resolvers/project_pipelines_resolver.rb +++ b/app/graphql/resolvers/project_pipelines_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class ProjectPipelinesResolver < BaseResolver include ResolvesPipelines diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb index ec115bad896..ac7c9b0ce2e 100644 --- a/app/graphql/resolvers/project_resolver.rb +++ b/app/graphql/resolvers/project_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class ProjectResolver < BaseResolver prepend FullPathResolver diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index b45a845f74f..cf43fea45e6 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseEnum < GraphQL::Schema::Enum end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index c5740a334d7..2b2ea64c00b 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseField < GraphQL::Schema::Field prepend Gitlab::Graphql::Authorize diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb index 309e336e6c8..aebed035d3b 100644 --- a/app/graphql/types/base_input_object.rb +++ b/app/graphql/types/base_input_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseInputObject < GraphQL::Schema::InputObject end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb index 69e72dc5808..3451a195c33 100644 --- a/app/graphql/types/base_interface.rb +++ b/app/graphql/types/base_interface.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module BaseInterface include GraphQL::Schema::Interface diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 754adf4c04d..82b78abd573 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseObject < GraphQL::Schema::Object prepend Gitlab::Graphql::Present diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb index c0aa38be239..719bc808f47 100644 --- a/app/graphql/types/base_scalar.rb +++ b/app/graphql/types/base_scalar.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseScalar < GraphQL::Schema::Scalar end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb index 36337fc6ee5..30a5668c0bb 100644 --- a/app/graphql/types/base_union.rb +++ b/app/graphql/types/base_union.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseUnion < GraphQL::Schema::Union end diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb index 2c12e5001d8..c19ddf5bb25 100644 --- a/app/graphql/types/ci/pipeline_status_enum.rb +++ b/app/graphql/types/ci/pipeline_status_enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module Ci class PipelineStatusEnum < BaseEnum diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index bbb7d9354d0..2bbffad4563 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module Ci class PipelineType < BaseObject diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 88cd2adc6dc..fb740b6fb1c 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class MergeRequestType < BaseObject expose_permissions Types::PermissionTypes::MergeRequest diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb index 934ed572e56..26a71e2bfbb 100644 --- a/app/graphql/types/permission_types/base_permission_type.rb +++ b/app/graphql/types/permission_types/base_permission_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes class BasePermissionType < BaseObject diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb index 942539c7cf7..73e44a33eba 100644 --- a/app/graphql/types/permission_types/ci/pipeline.rb +++ b/app/graphql/types/permission_types/ci/pipeline.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes module Ci diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index 5c21f6ee9c6..13995d3ea8f 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes class MergeRequest < BasePermissionType diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index 755699a4415..066ce64a254 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes class Project < BasePermissionType diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 97707215b4e..7b879608b34 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class ProjectType < BaseObject expose_permissions Types::PermissionTypes::Project diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 010ec2d7942..7c41716b82a 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class QueryType < BaseObject graphql_name 'Query' diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb index 2333d82ad1e..f045a50e672 100644 --- a/app/graphql/types/time_type.rb +++ b/app/graphql/types/time_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class TimeType < BaseScalar graphql_name 'Time' diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 1f67c045d1c..56f6686da57 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -191,7 +191,7 @@ module IssuablesHelper output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) - output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block prepend-left-8") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") output.join.html_safe diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index f2cd676bb1b..0d638b850b4 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -74,14 +74,21 @@ module MarkupHelper # the tag contents are truncated without removing the closing tag. def first_line_in_markdown(object, attribute, max_chars = nil, options = {}) md = markdown_field(object, attribute, options) + return nil unless md.present? - text = truncate_visible(md, max_chars || md.length) if md.present? + tags = %w(a gl-emoji b pre code p span) + tags << 'img' if options[:allow_images] - sanitize( + text = truncate_visible(md, max_chars || md.length) + text = sanitize( text, - tags: %w(a img gl-emoji b pre code p span), + tags: tags, attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version'] ) + + # since <img> tags are stripped, this can leave empty <a> tags hanging around + # (as our markdown wraps images in links) + options[:allow_images] ? text : strip_empty_link_tags(text).html_safe end def markdown(text, context = {}) @@ -235,6 +242,16 @@ module MarkupHelper end end + def strip_empty_link_tags(text) + scrubber = Loofah::Scrubber.new do |node| + node.remove if node.name == 'a' && node.content.blank? + end + + # Use `Loofah` directly instead of `sanitize` + # as we still use the `rails-deprecated_sanitizer` gem + Loofah.fragment(text).scrub!(scrubber).to_s + end + def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: 'body' }) content_tag :button, 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..eabb41c29d7 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` @@ -63,6 +72,9 @@ module Ci .project_type end + scope :order_contacted_at_asc, -> { order(contacted_at: :asc) } + scope :order_created_at_desc, -> { order(created_at: :desc) } + validate :tag_constraints validates :access_level, presence: true validates :runner_type, presence: true @@ -115,6 +127,14 @@ module Ci ONLINE_CONTACT_TIMEOUT.ago end + def self.order_by(order) + if order == 'contacted_asc' + order_contacted_at_asc + else + order_created_at_desc + end + end + def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? end diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 2371b0237d8..3d84eeed5a8 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -73,19 +73,10 @@ module Clusters "clientSecret" => oauth_application.secret, "callbackUrl" => callback_url } - }, - "singleuser" => { - "extraEnv" => { - "GITLAB_PROJECT_ID" => project_id - } } } end - def project_id - cluster&.project&.id - end - def gitlab_url Gitlab.config.gitlab.url end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 744f7f48dc8..58761fce952 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,18 +2,17 @@ module ProtectedBranchAccess extend ActiveSupport::Concern + include ProtectedRefAccess included do - include ProtectedRefAccess - belongs_to :protected_branch delegate :project, to: :protected_branch + end - def check_access(user) - return false if access_level == Gitlab::Access::NO_ACCESS + def check_access(user) + return false if access_level == Gitlab::Access::NO_ACCESS - super - end + super end end diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb index 04bd54d6b1c..3f5696c0749 100644 --- a/app/models/concerns/protected_tag_access.rb +++ b/app/models/concerns/protected_tag_access.rb @@ -2,10 +2,9 @@ module ProtectedTagAccess extend ActiveSupport::Concern + include ProtectedRefAccess included do - include ProtectedRefAccess - belongs_to :protected_tag delegate :project, to: :protected_tag diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb index 4e39f60b0d2..067e14dda1c 100644 --- a/app/models/dashboard_group_milestone.rb +++ b/app/models/dashboard_group_milestone.rb @@ -13,7 +13,7 @@ class DashboardGroupMilestone < GlobalMilestone end def self.build_collection(groups) - MilestonesFinder.new(group_ids: groups.pluck(:id)).execute.map { |m| new(m) } # rubocop: disable CodeReuse/Finder + MilestonesFinder.new(group_ids: groups.select(:id)).execute.map { |m| new(m) } # rubocop: disable CodeReuse/Finder end override :group_milestone? diff --git a/app/models/user.rb b/app/models/user.rb index dac8779488d..d68108a8e8e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -161,6 +161,7 @@ class User < ActiveRecord::Base validates :notification_email, presence: true validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true + validates :commit_email, email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email } validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, @@ -173,12 +174,15 @@ class User < ActiveRecord::Base validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? + validate :owns_commit_email, if: :commit_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } before_validation :sanitize_attrs before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? + before_validation :set_commit_email, if: :commit_email_changed? before_save :set_public_email, if: :public_email_changed? # in case validation is skipped + before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } @@ -619,6 +623,32 @@ class User < ActiveRecord::Base errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) end + def owns_commit_email + return if read_attribute(:commit_email).blank? + + errors.add(:commit_email, "is not an email you own") unless verified_emails.include?(commit_email) + end + + # Define commit_email-related attribute methods explicitly instead of relying + # on ActiveRecord to provide them. Some of the specs use the current state of + # the model code but an older database schema, so we need to guard against the + # possibility of the commit_email column not existing. + + def commit_email + return unless has_attribute?(:commit_email) + + # The commit email is the same as the primary email if undefined + super.presence || self.email + end + + def commit_email=(email) + super if has_attribute?(:commit_email) + end + + def commit_email_changed? + has_attribute?(:commit_email) && super + end + # see if the new email is already a verified secondary email def check_for_verified_email skip_reconfirmation! if emails.confirmed.where(email: self.email).any? @@ -873,10 +903,17 @@ class User < ActiveRecord::Base end end + def set_commit_email + if commit_email.blank? || verified_emails.exclude?(commit_email) + self.commit_email = nil + end + end + def update_secondary_emails! set_notification_email set_public_email - save if notification_email_changed? || public_email_changed? + set_commit_email + save if notification_email_changed? || public_email_changed? || commit_email_changed? end def set_projects_limit diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb deleted file mode 100644 index 15eda56cac6..00000000000 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -## -# TODO: -# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb -# We should dry up those classes not to repeat the same code. -# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. -module Ci - class FetchKubernetesTokenService - attr_reader :api_url, :ca_pem, :username, :password - - def initialize(api_url, ca_pem, username, password) - @api_url = api_url - @ca_pem = ca_pem - @username = username - @password = password - end - - def execute - read_secrets.each do |secret| - name = secret.dig('metadata', 'name') - if /default-token/ =~ name - token_base64 = secret.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - end - end - - nil - end - - private - - def read_secrets - kubeclient = build_kubeclient! - - kubeclient.get_secrets.as_json - rescue Kubeclient::HttpError => err - raise err unless err.error_code == 404 - - [] - end - - def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && username && password - - ::Kubeclient::Client.new( - join_api_url(api_path), - api_version, - auth_options: { username: username, password: password }, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - - def kubeclient_ssl_options - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - end -end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 264419501dc..3ae0a4a19d0 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -9,17 +9,24 @@ module Clusters @provider = provider configure_provider + create_gitlab_service_account! configure_kubernetes cluster.save! rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue Kubeclient::HttpError => e + provider.make_errored!("Failed to run Kubeclient: #{e.message}") rescue ActiveRecord::RecordInvalid => e provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}") end private + def create_gitlab_service_account! + Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute + end + def configure_provider provider.endpoint = gke_cluster.endpoint provider.status_event = :make_created @@ -32,15 +39,54 @@ module Clusters ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), username: gke_cluster.master_auth.username, password: gke_cluster.master_auth.password, + authorization_type: authorization_type, token: request_kubernetes_token) end def request_kubernetes_token - Ci::FetchKubernetesTokenService.new( + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute + end + + def authorization_type + create_rbac_cluster? ? 'rbac' : 'abac' + end + + def create_rbac_cluster? + !provider.legacy_abac? + end + + def kube_client + @kube_client ||= build_kube_client!( 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), gke_cluster.master_auth.username, - gke_cluster.master_auth.password).execute + gke_cluster.master_auth.password, + api_groups: ['api', 'apis/rbac.authorization.k8s.io'] + ) + end + + def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1') + raise "Incomplete settings" unless api_url && username && password + + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, + api_version, + auth_options: { username: username, password: password }, + ssl_options: kubeclient_ssl_options(ca_pem), + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_ssl_options(ca_pem) + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts end def gke_cluster diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb new file mode 100644 index 00000000000..d014d73b3e8 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + SERVICE_ACCOUNT_NAME = 'gitlab' + SERVICE_ACCOUNT_NAMESPACE = 'default' + SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token' + CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' + CLUSTER_ROLE_NAME = 'cluster-admin' + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb new file mode 100644 index 00000000000..d17744591e6 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class CreateServiceAccountService + attr_reader :kubeclient, :rbac + + def initialize(kubeclient, rbac:) + @kubeclient = kubeclient + @rbac = rbac + end + + def execute + kubeclient.create_service_account(service_account_resource) + kubeclient.create_secret(service_account_token_resource) + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac + end + + private + + def service_account_resource + Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate + end + + def service_account_token_resource + Gitlab::Kubernetes::ServiceAccountToken.new( + SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate + end + + def cluster_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + CLUSTER_ROLE_BINDING_NAME, + CLUSTER_ROLE_NAME, + subjects + ).generate + end + + def service_account_name + SERVICE_ACCOUNT_NAME + end + + def service_account_namespace + SERVICE_ACCOUNT_NAMESPACE + end + end + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..9e09345c8dc --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class FetchKubernetesTokenService + attr_reader :kubeclient + + def initialize(kubeclient) + @kubeclient = kubeclient + end + + def execute + token_base64 = get_secret&.dig('data', 'token') + Base64.decode64(token_base64) if token_base64 + end + + private + + def get_secret + kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json + rescue Kubeclient::HttpError => err + raise err unless err.error_code == 404 + + nil + end + end + end + end +end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb index ab1bf9c64f6..80040511ec2 100644 --- a/app/services/clusters/gcp/provision_service.rb +++ b/app/services/clusters/gcp/provision_service.rb @@ -27,7 +27,9 @@ module Clusters provider.zone, provider.cluster.name, provider.num_nodes, - machine_type: provider.machine_type) + machine_type: provider.machine_type, + legacy_abac: provider.legacy_abac + ) unless operation.status == 'PENDING' || operation.status == 'RUNNING' return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 43937b01339..9c15226f0ec 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -1,5 +1,5 @@ -%tr{ id: dom_id(runner) } - %td +.gl-responsive-table-row{ id: dom_id(runner) } + = render layout: 'runner_table_cell', locals: { label: _('Type') } do - if runner.instance_type? %span.badge.badge-success shared - elsif runner.group_type? @@ -11,41 +11,50 @@ - unless runner.active? %span.badge.badge-danger paused - %td - = link_to admin_runner_path(runner) do - = runner.short_sha - %td + = render layout: 'runner_table_cell', locals: { label: _('Runner token') } do + = link_to runner.short_sha, admin_runner_path(runner) + + = render layout: 'runner_table_cell', locals: { label: _('Description') } do = runner.description - %td + + = render layout: 'runner_table_cell', locals: { label: _('Version') } do = runner.version - %td + + = render layout: 'runner_table_cell', locals: { label: _('IP Address') } do = runner.ip_address - %td + + = render layout: 'runner_table_cell', locals: { label: _('Projects') } do - if runner.instance_type? || runner.group_type? - n/a + = _('n/a') - else = runner.projects.count(:all) - %td - #{runner.builds.count(:all)} - %td + + = render layout: 'runner_table_cell', locals: { label: _('Jobs') } do + = runner.builds.count(:all) + + = render layout: 'runner_table_cell', locals: { label: _('Tags') } do - runner.tag_list.sort.each do |tag| %span.badge.badge-primary = tag - %td + + = render layout: 'runner_table_cell', locals: { label: _('Last contact') } do - if runner.contacted_at = time_ago_with_tooltip runner.contacted_at - else - Never - %td.admin-runner-btn-group-cell - .float-right.btn-group - = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do - = icon('pencil') - - - if runner.active? - = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-sm btn-default has-tooltip', title: 'Pause', ref: 'tooltip', aria: { label: 'Pause' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do - = icon('pause') - - else - = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-sm has-tooltip', title: 'Resume', ref: 'tooltip', aria: { label: 'Resume' }, data: { placement: 'top', container: 'body'} do - = icon('play') - = link_to [:admin, runner], method: :delete, class: 'btn btn-danger btn-sm has-tooltip', title: 'Remove', ref: 'tooltip', aria: { label: 'Remove' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do - = icon('remove') + = _('Never') + + .table-section.table-button-footer.section-10 + .btn-group.table-action-buttons + .btn-group + = link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do + = icon('pencil') + .btn-group + - if runner.active? + = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do + = icon('pause') + - else + = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do + = icon('play') + .btn-group + = link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do + = icon('remove') diff --git a/app/views/admin/runners/_runner_table_cell.html.haml b/app/views/admin/runners/_runner_table_cell.html.haml new file mode 100644 index 00000000000..78526ee6d23 --- /dev/null +++ b/app/views/admin/runners/_runner_table_cell.html.haml @@ -0,0 +1,4 @@ +.table-section.section-10 + .table-mobile-header{ role: 'rowheader' }= label + .table-mobile-content + = yield diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml new file mode 100644 index 00000000000..b201e6bf10e --- /dev/null +++ b/app/views/admin/runners/_sort_dropdown.html.haml @@ -0,0 +1,11 @@ +- sorted_by = sort_options_hash[@sort] + +.dropdown.inline.prepend-left-10 + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } + = sorted_by + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort + %li + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) + = sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date, label: true), sorted_by) + diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 9280ff4d478..4dc076c95c5 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,77 +1,107 @@ -- breadcrumb_title "Runners" +- breadcrumb_title _('Runners') - @no_container = true %div{ class: container_class } .bs-callout %p - A 'Runner' is a process which runs a job. - You can setup as many Runners as you need. + = (_"A 'Runner' is a process which runs a job. You can setup as many Runners as you need.") %br - Runners can be placed on separate users, servers, even on your local machine. + = _('Runners can be placed on separate users, servers, even on your local machine.') %br %div - %span Each Runner can be in one of the following states: + %span= _('Each Runner can be in one of the following states:') %ul %li %span.badge.badge-success shared - \- Runner runs jobs from all unassigned projects + \- + = _('Runner runs jobs from all unassigned projects') %li %span.badge.badge-success group - \- Runner runs jobs from all unassigned projects in its group + \- + = _('Runner runs jobs from all unassigned projects in its group') %li %span.badge.badge-info specific - \- Runner runs jobs from assigned projects + \- + = _('Runner runs jobs from assigned projects') %li %span.badge.badge-warning locked - \- Runner cannot be assigned to other projects + \- + = _('Runner cannot be assigned to other projects') %li %span.badge.badge-danger paused - \- Runner will not receive any new jobs + \- + = _('Runner will not receive any new jobs') .bs-callout.clearfix .float-left %p - You can reset runners registration token by pressing a button below. + = _('You can reset runners registration token by pressing a button below.') .prepend-top-10 - = button_to _("Reset runners registration token"), reset_runners_token_admin_application_settings_path, + = button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path, method: :put, class: 'btn btn-default', - data: { confirm: _("Are you sure you want to reset registration token?") } + data: { confirm: _('Are you sure you want to reset registration token?') } = render partial: 'ci/runner/how_to_setup_shared_runner', locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } - .append-bottom-20.clearfix - .float-left - = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do - .form-group - = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false - = submit_tag 'Search', class: 'btn' - - .float-right.light - Runners currently online: #{@active_runners_cnt} + .bs-callout + %p + = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count } - %br + .row-content-block.second-block + = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do + .filtered-search-wrapper + .filtered-search-box + = dropdown_tag(custom_icon('icon_history'), + options: { wrapper_class: 'filtered-search-history-dropdown-wrapper', + toggle_class: 'filtered-search-history-dropdown-toggle-button', + dropdown_class: 'filtered-search-history-dropdown', + content_class: 'filtered-search-history-dropdown-content', + title: _('Recent searches') }) do + .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } } + .filtered-search-box-input-container.droplab-dropdown + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } } + #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } + = button_tag class: %w[btn btn-link] do + = icon('search') + %span + = _('Press Enter or click to search') + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + = button_tag class: %w[btn btn-link] do + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %i.fa{ class: "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + - Ci::Runner::AVAILABLE_STATUSES.each do |status| + %li.filter-dropdown-item{ data: { value: status } } + = button_tag class: %w[btn btn-link] do + = status.titleize + = button_tag class: %w[clear-search hidden] do + = icon('times') + .filter-dropdown-container + = render 'sort_dropdown' - if @runners.any? - .runners-content + .runners-content.content-list .table-holder - %table.table - %thead - %tr - %th Type - %th Runner token - %th Description - %th Version - %th IP Address - %th Projects - %th Jobs - %th Tags - %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc')) - %th + .gl-responsive-table-row.table-row-header{ role: 'row' } + - [_('Type'), _('Runner token'), _('Description'), _('Version'), _('IP Address'), _('Projects'), _('Jobs'), _('Tags'), _('Last contact')].each do |label| + .table-section.section-10{ role: 'rowheader' }= label - - @runners.each do |runner| - = render "admin/runners/runner", runner: runner - = paginate @runners, theme: "gitlab" + - @runners.each do |runner| + = render 'admin/runners/runner', runner: runner + = paginate @runners, theme: 'gitlab' - else - .nothing-here-block No runners found + .nothing-here-block= _('No runners found') diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 04a19ab14dd..c8faf2b3af3 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -22,7 +22,9 @@ .account-well.append-bottom-default %ul %li - Your Primary Email will be used for avatar detection and web based operations, such as edits and merges. + Your Primary Email will be used for avatar detection. + %li + Your Commit Email will be used for web based operations, such as edits and merges. %li Your Notification Email will be used for account notifications. %li @@ -34,6 +36,8 @@ = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } %span.float-right %span.badge.badge-success Primary email + - if @primary_email === current_user.commit_email + %span.badge.badge-info Commit email - if @primary_email === current_user.public_email %span.badge.badge-info Public email - if @primary_email === current_user.notification_email @@ -42,6 +46,8 @@ %li = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? } %span.float-right + - if email.email === current_user.commit_email + %span.badge.badge-info Commit email - if email.email === current_user.public_email %span.badge.badge-info Public email - if email.email === current_user.notification_email diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 0a1ee648d97..51f5ecf2166 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -91,6 +91,9 @@ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") }, control_class: 'select2' + = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email), + { help: 'This email will be used for web based operations, such as edits and merges.' }, + control_class: 'select2' = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, { help: s_("Profiles|This feature is experimental and translations are not complete yet.") }, control_class: 'select2' diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 9133de6559d..0222bbf7338 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -61,5 +61,15 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank' + .form-group = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml index 877e0cc876c..be84f2ae67c 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -37,5 +37,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 1f81e024ab9..f497f5b606c 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -33,6 +33,7 @@ .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank' .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index d8580ad8ab4..ac8d58c0bfe 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -34,4 +34,4 @@ = form.label :due_date, "Due date", class: "col-form-label col-md-2 col-lg-4" .col-8 .issuable-form-select-holder - = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off' |