diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-13 15:07:53 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-13 15:07:53 +0000 |
commit | a5ab3467a705b62911feacc3cf627fdbb00aa198 (patch) | |
tree | 65143ce13405efccb922fc428624ad57c38b6efa /app | |
parent | eb30dd6e28f6fc9eb8021d205f6ed84511f001e2 (diff) | |
download | gitlab-ce-a5ab3467a705b62911feacc3cf627fdbb00aa198.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
32 files changed, 347 insertions, 129 deletions
diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js index e77a7cf8e0a..0c732922e81 100644 --- a/app/assets/javascripts/pages/groups/group_members/index/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index/index.js @@ -3,9 +3,12 @@ import Members from 'ee_else_ce/members'; import memberExpirationDate from '~/member_expiration_date'; import UsersSelect from '~/users_select'; +import groupsSelect from '~/groups_select'; document.addEventListener('DOMContentLoaded', () => { memberExpirationDate(); + memberExpirationDate('.js-access-expiration-date-groups'); new Members(); + groupsSelect(); new UsersSelect(); }); diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index 402763e2e21..55a6a1ace55 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -1,6 +1,6 @@ <script> -import { mapActions } from 'vuex'; -import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton, GlCard } from '@gitlab/ui'; import { s__, __, sprintf } from '~/locale'; import { NAME_REGEX_LENGTH } from '../constants'; import { mapComputed } from '~/vuex_shared/bindings'; @@ -12,19 +12,25 @@ export default { GlFormSelect, GlFormTextarea, GlButton, + GlCard, }, labelsConfig: { cols: 3, align: 'right', }, computed: { - ...mapComputed('settings', 'updateSettings', [ - 'enabled', - 'cadence', - 'older_than', - 'keep_n', - 'name_regex', - ]), + ...mapState(['formOptions']), + ...mapComputed( + [ + 'enabled', + { key: 'cadence', getter: 'getCadence' }, + { key: 'older_than', getter: 'getOlderThan' }, + { key: 'keep_n', getter: 'getKeepN' }, + 'name_regex', + ], + 'updateSettings', + 'settings', + ), policyEnabledText() { return this.enabled ? __('enabled') : __('disabled'); }, @@ -66,12 +72,12 @@ export default { </script> <template> - <div class="card"> - <form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings"> - <div class="card-header"> + <form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings"> + <gl-card> + <template #header> {{ s__('ContainerRegistry|Tag expiration policy') }} - </div> - <div class="card-body"> + </template> + <template> <gl-form-group id="expiration-policy-toggle-group" :label-cols="$options.labelsConfig.cols" @@ -92,9 +98,10 @@ export default { label-for="expiration-policy-interval" :label="s__('ContainerRegistry|Expiration interval:')" > - <gl-form-select id="expiration-policy-interval" v-model="older_than"> - <option value="1">{{ __('Option 1') }}</option> - <option value="2">{{ __('Option 2') }}</option> + <gl-form-select id="expiration-policy-interval" v-model="older_than" :disabled="!enabled"> + <option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key"> + {{ option.label }} + </option> </gl-form-select> </gl-form-group> @@ -105,9 +112,10 @@ export default { label-for="expiration-policy-schedule" :label="s__('ContainerRegistry|Expiration schedule:')" > - <gl-form-select id="expiration-policy-schedule" v-model="cadence"> - <option value="1">{{ __('Option 1') }}</option> - <option value="2">{{ __('Option 2') }}</option> + <gl-form-select id="expiration-policy-schedule" v-model="cadence" :disabled="!enabled"> + <option v-for="option in formOptions.cadence" :key="option.key" :value="option.key"> + {{ option.label }} + </option> </gl-form-select> </gl-form-group> @@ -118,9 +126,10 @@ export default { label-for="expiration-policy-latest" :label="s__('ContainerRegistry|Expiration latest:')" > - <gl-form-select id="expiration-policy-latest" v-model="keep_n"> - <option value="1">{{ __('Option 1') }}</option> - <option value="2">{{ __('Option 2') }}</option> + <gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled"> + <option v-for="option in formOptions.keepN" :key="option.key" :value="option.key"> + {{ option.label }} + </option> </gl-form-select> </gl-form-group> @@ -140,19 +149,30 @@ export default { v-model="name_regex" :placeholder="nameRegexPlaceholder" :state="nameRegexState" + :disabled="!enabled" trim /> <template #description> <span ref="regex-description" v-html="regexHelpText"></span> </template> </gl-form-group> - </div> - <div class="card-footer text-right"> - <gl-button ref="cancel-button" type="reset">{{ __('Cancel') }}</gl-button> - <gl-button ref="save-button" type="submit" :disabled="formIsValid" variant="success"> - {{ __('Save Expiration Policy') }} - </gl-button> - </div> - </form> - </div> + </template> + <template #footer> + <div class="d-flex justify-content-end"> + <gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{ + __('Cancel') + }}</gl-button> + <gl-button + ref="save-button" + type="submit" + :disabled="formIsValid" + variant="success" + class="d-block" + > + {{ __('Save expiration policy') }} + </gl-button> + </div> + </template> + </gl-card> + </form> </template> diff --git a/app/assets/javascripts/registry/settings/store/getters.js b/app/assets/javascripts/registry/settings/store/getters.js new file mode 100644 index 00000000000..fc32a9f08e4 --- /dev/null +++ b/app/assets/javascripts/registry/settings/store/getters.js @@ -0,0 +1,8 @@ +import { findDefaultOption } from '../utils'; + +export const getCadence = state => + state.settings.cadence || findDefaultOption(state.formOptions.cadence); +export const getKeepN = state => + state.settings.keep_n || findDefaultOption(state.formOptions.keepN); +export const getOlderThan = state => + state.settings.older_than || findDefaultOption(state.formOptions.olderThan); diff --git a/app/assets/javascripts/registry/settings/store/index.js b/app/assets/javascripts/registry/settings/store/index.js index 91a35aac149..c2500454d8e 100644 --- a/app/assets/javascripts/registry/settings/store/index.js +++ b/app/assets/javascripts/registry/settings/store/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import * as actions from './actions'; import mutations from './mutations'; +import * as getters from './getters'; import state from './state'; Vue.use(Vuex); @@ -11,6 +12,7 @@ export const createStore = () => state, actions, mutations, + getters, }); export default createStore(); diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js index b8384fd4a45..25a67cc6973 100644 --- a/app/assets/javascripts/registry/settings/store/mutations.js +++ b/app/assets/javascripts/registry/settings/store/mutations.js @@ -3,6 +3,11 @@ import * as types from './mutation_types'; export default { [types.SET_INITIAL_STATE](state, initialState) { state.projectId = initialState.projectId; + state.formOptions = { + cadence: JSON.parse(initialState.cadenceOptions), + keepN: JSON.parse(initialState.keepNOptions), + olderThan: JSON.parse(initialState.olderThanOptions), + }; }, [types.UPDATE_SETTINGS](state, settings) { state.settings = { ...state.settings, ...settings }; diff --git a/app/assets/javascripts/registry/settings/store/state.js b/app/assets/javascripts/registry/settings/store/state.js index c3a26083c9f..50c882e1839 100644 --- a/app/assets/javascripts/registry/settings/store/state.js +++ b/app/assets/javascripts/registry/settings/store/state.js @@ -23,4 +23,8 @@ export default () => ({ * Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel' */ original: {}, + /* + * Contains the options used to populate the form selects + */ + formOptions: {}, }); diff --git a/app/assets/javascripts/registry/settings/utils.js b/app/assets/javascripts/registry/settings/utils.js new file mode 100644 index 00000000000..75af401e96d --- /dev/null +++ b/app/assets/javascripts/registry/settings/utils.js @@ -0,0 +1,6 @@ +export const findDefaultOption = options => { + const item = options.find(o => o.default); + return item ? item.key : null; +}; + +export default () => {}; diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js index 51035ebc8a8..817a90f8149 100644 --- a/app/assets/javascripts/vuex_shared/bindings.js +++ b/app/assets/javascripts/vuex_shared/bindings.js @@ -1,9 +1,29 @@ -export const mapComputed = (root, updateFn, list) => { +/** + * Returns computed properties two way bound to vuex + * + * @param {(string[]|Object[])} list - list of string matching state keys or list objects + * @param {string} list[].key - the key matching the key present in the vuex state + * @param {string} list[].getter - the name of the getter, leave it empty to not use a getter + * @param {string} list[].updateFn - the name of the action, leave it empty to use the default action + * @param {string} defaultUpdateFn - the default function to dispatch + * @param {string} root - the key of the state where to search fo they keys described in list + * @returns {Object} a dictionary with all the computed properties generated + */ +export const mapComputed = (list, defaultUpdateFn, root) => { const result = {}; - list.forEach(key => { + list.forEach(item => { + const [getter, key, updateFn] = + typeof item === 'string' + ? [false, item, defaultUpdateFn] + : [item.getter, item.key, item.updateFn || defaultUpdateFn]; result[key] = { get() { - return this.$store.state[root][key]; + if (getter) { + return this.$store.getters[getter]; + } else if (root) { + return this.$store.state[root][key]; + } + return this.$store.state[key]; }, set(value) { this.$store.dispatch(updateFn, { [key]: value }); diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index ae92a2fbd7b..54bca80194f 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -3,7 +3,7 @@ border-bottom: 1px solid $border-color; } -.users-project-form { +.invite-users-form { .btn-success { margin-right: 10px; } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 58f088c6335..3047ee02680 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -5,6 +5,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController before_action :set_application_setting before_action :whitelist_query_limiting, only: [:usage_data] + before_action :validate_self_monitoring_feature_flag_enabled, only: [ + :create_self_monitoring_project, + :status_create_self_monitoring_project, + :delete_self_monitoring_project, + :status_delete_self_monitoring_project + ] before_action do push_frontend_feature_flag(:self_monitoring_project) @@ -74,8 +80,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def create_self_monitoring_project - return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project) - job_id = SelfMonitoringProjectCreateWorker.perform_async render status: :accepted, json: { @@ -85,8 +89,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def status_create_self_monitoring_project - return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project) - job_id = params[:job_id].to_s unless job_id.length <= PARAM_JOB_ID_MAX_SIZE @@ -97,23 +99,66 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end if Gitlab::CurrentSettings.instance_administration_project_id.present? - render status: :ok, json: self_monitoring_data + return render status: :ok, json: self_monitoring_data elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id) ::Gitlab::PollingInterval.set_header(response, interval: 3_000) - render status: :accepted, json: { message: _('Job is in progress') } + return render status: :accepted, json: { + message: _('Job to create self-monitoring project is in progress') + } + end + + render status: :bad_request, json: { + message: _('Self-monitoring project does not exist. Please check logs ' \ + 'for any error messages') + } + end + + def delete_self_monitoring_project + job_id = SelfMonitoringProjectDeleteWorker.perform_async + + render status: :accepted, json: { + job_id: job_id, + monitor_status: status_delete_self_monitoring_project_admin_application_settings_path + } + end + + def status_delete_self_monitoring_project + job_id = params[:job_id].to_s + + unless job_id.length <= PARAM_JOB_ID_MAX_SIZE + return render status: :bad_request, json: { + message: _('Parameter "job_id" cannot exceed length of %{job_id_max_size}' % + { job_id_max_size: PARAM_JOB_ID_MAX_SIZE }) + } + end - else - render status: :bad_request, json: { - message: _('Self-monitoring project does not exist. Please check logs ' \ - 'for any error messages') + if Gitlab::CurrentSettings.instance_administration_project_id.nil? + return render status: :ok, json: { + message: _('Self-monitoring project has been successfully deleted') + } + + elsif SelfMonitoringProjectDeleteWorker.in_progress?(job_id) + ::Gitlab::PollingInterval.set_header(response, interval: 3_000) + + return render status: :accepted, json: { + message: _('Job to delete self-monitoring project is in progress') } end + + render status: :bad_request, json: { + message: _('Self-monitoring project was not deleted. Please check logs ' \ + 'for any error messages') + } end private + def validate_self_monitoring_feature_flag_enabled + self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project) + end + def self_monitoring_data { project_id: Gitlab::CurrentSettings.instance_administration_project_id, diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb index 2e9905997db..c92b1cecaaa 100644 --- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb +++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb @@ -18,7 +18,7 @@ module RequiresWhitelistedMonitoringClient # debugging purposes return true if Rails.env.development? && request.local? - ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.client_ip) } + ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.instance.client_ip) } end def ip_whitelist diff --git a/app/controllers/groups/group_links_controller.rb b/app/controllers/groups/group_links_controller.rb index 7965311c5f1..6796a862c00 100644 --- a/app/controllers/groups/group_links_controller.rb +++ b/app/controllers/groups/group_links_controller.rb @@ -3,6 +3,7 @@ class Groups::GroupLinksController < Groups::ApplicationController before_action :check_feature_flag! before_action :authorize_admin_group! + before_action :group_link, only: [:update, :destroy] def create shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present? @@ -22,12 +23,35 @@ class Groups::GroupLinksController < Groups::ApplicationController redirect_to group_group_members_path(group) end + def update + @group_link.update(group_link_params) + end + + def destroy + Groups::GroupLinks::DestroyService.new(nil, nil).execute(@group_link) + + respond_to do |format| + format.html do + redirect_to group_group_members_path(group), status: :found + end + format.js { head :ok } + end + end + private + def group_link + @group_link ||= group.shared_with_group_links.find(params[:id]) + end + def group_link_create_params params.permit(:shared_group_access, :expires_at) end + def group_link_params + params.require(:group_link).permit(:group_access, :expires_at) + end + def check_feature_flag! render_404 unless Feature.enabled?(:share_group_with_group) end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index dcdf9aced1a..d1eed85fde6 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -20,28 +20,17 @@ class Groups::GroupMembersController < Groups::ApplicationController :override def index - can_manage_members = can?(current_user, :admin_group_member, @group) - @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] @members = find_members if can_manage_members - @invited_members = @members.invite - @invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present? - @invited_members = present_members(@invited_members.page(params[:invited_members_page]).per(MEMBER_PER_PAGE_LIMIT)) + @skip_groups = @group.related_group_ids + @invited_members = present_invited_members(@members) end @members = @members.non_invite - @members = @members.search(params[:search]) if params[:search].present? - @members = @members.sort_by_attribute(@sort) - - if can_manage_members && params[:two_factor].present? - @members = @members.filter_by_2fa(params[:two_factor]) - end - - @members = @members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT) - @members = present_members(@members) + @members = present_group_members(@members) @requesters = present_members( AccessRequestsFinder.new(@group).execute(current_user)) @@ -54,8 +43,30 @@ class Groups::GroupMembersController < Groups::ApplicationController private + def present_invited_members(members) + invited_members = members.invite + + if params[:search_invited].present? + invited_members = invited_members.search_invite_email(params[:search_invited]) + end + + present_members(invited_members + .page(params[:invited_members_page]) + .per(MEMBER_PER_PAGE_LIMIT)) + end + def find_members - GroupMembersFinder.new(@group).execute(include_relations: requested_relations) + filter_params = params.slice(:two_factor, :search).merge(sort: @sort) + GroupMembersFinder.new(@group, current_user).execute(include_relations: requested_relations, params: filter_params) + end + + def can_manage_members + can?(current_user, :admin_group_member, @group) + end + + def present_group_members(original_members) + members = original_members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT) + present_members(members) end end diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index d8739c350e4..05151b4f1f3 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -1,38 +1,64 @@ # frozen_string_literal: true class GroupMembersFinder < UnionFinder - def initialize(group) + # Params can be any of the following: + # two_factor: string. 'enabled' or 'disabled' are returning different set of data, other values are not effective. + # sort: string + # search: string + + def initialize(group, user = nil) @group = group + @user = user end # rubocop: disable CodeReuse/ActiveRecord - def execute(include_relations: [:inherited, :direct]) - group_members = @group.members + def execute(include_relations: [:inherited, :direct], params: {}) + group_members = group.members relations = [] return group_members if include_relations == [:direct] relations << group_members if include_relations.include?(:direct) - if include_relations.include?(:inherited) && @group.parent + if include_relations.include?(:inherited) && group.parent parents_members = GroupMember.non_request - .where(source_id: @group.ancestors.select(:id)) - .where.not(user_id: @group.users.select(:id)) + .where(source_id: group.ancestors.select(:id)) + .where.not(user_id: group.users.select(:id)) relations << parents_members end if include_relations.include?(:descendants) descendant_members = GroupMember.non_request - .where(source_id: @group.descendants.select(:id)) - .where.not(user_id: @group.users.select(:id)) + .where(source_id: group.descendants.select(:id)) + .where.not(user_id: group.users.select(:id)) relations << descendant_members end - find_union(relations, GroupMember) + members = find_union(relations, GroupMember) + filter_members(members, params) end # rubocop: enable CodeReuse/ActiveRecord + + private + + attr_reader :user, :group + + def filter_members(members, params) + members = members.search(params[:search]) if params[:search].present? + members = members.sort_by_attribute(params[:sort]) if params[:sort].present? + + if can_manage_members && params[:two_factor].present? + members = members.filter_by_2fa(params[:two_factor]) + end + + members + end + + def can_manage_members + Ability.allowed?(user, :admin_group_member, group) + end end GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder') diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index fe1f543ea1a..c439a6cfc2f 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -42,7 +42,7 @@ module Mutations if project_path.present? project = find_project!(project_path: project_path) elsif !can_create_personal_snippet? - raise_resource_not_avaiable_error! + raise_resource_not_available_error! end snippet = CreateSnippetService.new(project, diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ffea7d53b14..7115fd834fd 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -344,6 +344,12 @@ module ApplicationSettingsHelper 'status_create_self_monitoring_project_path' => status_create_self_monitoring_project_admin_application_settings_path, + 'delete_self_monitoring_project_path' => + delete_self_monitoring_project_admin_application_settings_path, + + 'status_delete_self_monitoring_project_path' => + status_delete_self_monitoring_project_admin_application_settings_path, + 'self_monitoring_project_exists' => Gitlab::CurrentSettings.instance_administration_project.present?, diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index a8f6c974bbd..1952325c504 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -4,6 +4,10 @@ module Groups::GroupMembersHelper def group_member_select_options { multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true } end + + def render_invite_member_for_group(group, default_access_level) + render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level + end end Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper') diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 90c54123597..4d0f9e530fb 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -85,7 +85,8 @@ module SelectsHelper first_user: opts[:first_user] && current_user ? current_user.username : false, current_user: opts[:current_user] || false, author_id: opts[:author_id] || '', - skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil + skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil, + qa_selector: opts[:qa_selector] || '' } end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3858adfc71f..a0a600a340e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -169,7 +169,11 @@ class ApplicationSetting < ApplicationRecord validates :gitaly_timeout_default, presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { + only_integer: true, + greater_than_or_equal_to: 0, + less_than_or_equal_to: Settings.gitlab.max_request_duration_seconds + } validates :gitaly_timeout_medium, presence: true, diff --git a/app/models/group.rb b/app/models/group.rb index 8289d4f099c..186253619fe 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -420,6 +420,12 @@ class Group < Namespace GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last end + def related_group_ids + [id, + *ancestors.pluck(:id), + *shared_with_group_links.pluck(:shared_with_group_id)] + end + def hashed_storage?(_feature) false end diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb index 4b279b7af5b..5a0d9b08cb0 100644 --- a/app/models/group_group_link.rb +++ b/app/models/group_group_link.rb @@ -20,4 +20,8 @@ class GroupGroupLink < ApplicationRecord def self.default_access Gitlab::Access::DEVELOPER end + + def human_access + Gitlab::Access.human_access(self.group_access) + end end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 0d3a2d4e398..b70c07a8386 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -21,6 +21,8 @@ class ProjectGroupLink < ApplicationRecord after_commit :refresh_group_members_authorized_projects + alias_method :shared_with_group, :group + def self.access_options Gitlab::Access.options end diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml index 1da02de0461..fac2de8811f 100644 --- a/app/views/admin/application_settings/_gitaly.html.haml +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -8,6 +8,9 @@ .form-text.text-muted Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced for git fetch/push operations or Sidekiq jobs. + This timeout should be less than the worker timeout. If a Gitaly call timeout would exceed the + worker timeout, the remaining time from the worker timeout would be used to avoid having to terminate + the worker. .form-group = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-bold' = f.number_field :gitaly_timeout_fast, class: 'form-control' diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml deleted file mode 100644 index 93dd8f48a60..00000000000 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -= form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f| - .row - .col-md-4.col-lg-6 - = users_select_tag(:user_ids, group_member_select_options) - .form-text.text-muted.append-bottom-10 - Search for members by name, username, or email, or invite new ones using their email address. - - .col-md-3.col-lg-2 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" - .form-text.text-muted.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions") - about role permissions - - .col-md-3.col-lg-2 - .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' - %i.clear-icon.js-clear-input - .form-text.text-muted.append-bottom-10 - On this date, the member(s) will automatically lose access to this group and all of its projects. - - .col-md-2 - = f.submit 'Add to group', class: "btn btn-success btn-block", data: { qa_selector: 'add_to_group_button' } diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 882fcc79421..6eb8a8947cc 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,17 +1,28 @@ -- page_title _("Members") +- page_title _("Group members") - can_manage_members = can?(current_user, :admin_group_member, @group) - show_invited_members = can_manage_members && @invited_members.exists? - pending_active = params[:search_invited].present? +- total_count = @members.count + @group.shared_with_group_links.count .project-members-page.prepend-top-default %h4 - = _("Members") + = _("Group members") %hr - if can_manage_members - .project-members-new.append-bottom-default - %p.clearfix - = _("Add new member to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } - = render "new_group_member" + - if Feature.enabled?(:share_group_with_group) + %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } + %li.nav-tab{ role: 'presentation' } + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member") + %li.nav-tab{ role: 'presentation' } + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group") + .tab-content.gitlab-tab-content + .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } + = render_invite_member_for_group(@group, @group_member.access_level) + - if Feature.enabled?(:share_group_with_group) + .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } + = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access' + - else + = render_invite_member_for_group(@group, @group_member.access_level) = render 'shared/members/requests', membership_source: @group, requesters: @requesters @@ -19,10 +30,10 @@ %ul.nav-links.mobile-separator.nav.nav-tabs.clearfix %li.nav-item - = link_to "#existing_members", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do + = link_to "#existing_shares", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do %span - = _("Existing") - %span.badge.badge-pill= @members.total_count + = _("Existing shares") + %span.badge.badge-pill= total_count - if show_invited_members %li.nav-item = link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do @@ -31,7 +42,16 @@ %span.badge.badge-pill= @invited_members.total_count .tab-content - #existing_members.tab-pane{ :class => ("active" unless pending_active) } + #existing_shares.tab-pane{ :class => ("active" unless pending_active) } + - if @group.shared_with_group_links.any? + .card.card-without-border + .d-flex.flex-column.flex-md-row.row-content-block.second-block + %span.flex-grow-1.align-self-md-center.col-form-label + = _("Groups with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } + %ul.content-list.members-list{ data: { qa_selector: "groups_list" } } + - can_admin_member = can?(current_user, :admin_group_member, @group) + - @group.shared_with_group_links.each do |group_link| + = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: group_group_link_path(@group, group_link) .card.card-without-border .d-flex.flex-column.flex-md-row.row-content-block.second-block %span.flex-grow-1.align-self-md-center.col-form-label @@ -46,7 +66,7 @@ = label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2' = render 'shared/members/filter_2fa_dropdown' = render 'shared/members/sort_dropdown' - %ul.content-list.members-list + %ul.content-list.members-list{ data: { qa_selector: "members_list" } } = render partial: 'shared/members/member', collection: @members, as: :member = paginate @members, theme: 'gitlab' diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 00321014f91..353c36d0fed 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -3,4 +3,6 @@ = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) } %span.badge.badge-pill= group_links.size %ul.content-list.members-list - = render partial: 'shared/members/group', collection: group_links, as: :group_link + - can_admin_member = can?(current_user, :admin_project_member, @project) + - @group_links.each do |group_link| + = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: project_group_link_path(@project, group_link) diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 5310c1fad01..5d8005b2e2a 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -13,5 +13,5 @@ %button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") } = icon("search") = render 'shared/members/sort_dropdown' - %ul.content-list.members-list.qa-members-list + %ul.content-list.members-list{ data: { qa_selector: 'members_list' } } = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 24fe583a9b5..c24a9061146 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -23,13 +23,13 @@ .tab-content.gitlab-tab-content .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_member', tab_title: _('Invite member') + = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) } - = render 'projects/project_members/new_project_group', tab_title: _('Invite group') + = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access' - elsif !membership_locked? - .invite-member= render 'projects/project_members/new_project_member', tab_title: _('Invite member') + .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) - elsif @project.allowed_to_share_with_group? - .invite-group= render 'projects/project_members/new_project_group', tab_title: _('Invite group') + .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access' = render 'shared/members/requests', membership_source: @project, requesters: @requesters .clearfix diff --git a/app/views/projects/registry/settings/_index.haml b/app/views/projects/registry/settings/_index.haml index d023a083445..0e0341a9923 100644 --- a/app/views/projects/registry/settings/_index.haml +++ b/app/views/projects/registry/settings/_index.haml @@ -1 +1,4 @@ -#js-registry-settings{ data: { project_id: @project.id, } } +#js-registry-settings{ data: { project_id: @project.id, + cadence_options: cadence_options.to_json, + keep_n_options: keep_n_options.to_json, + older_than_options: older_than_options.to_json} } diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 18368ecc9ff..4aeeac87f3c 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -1,6 +1,7 @@ - group_link = local_assigns[:group_link] -- group = group_link.group -- can_admin_member = can?(current_user, :admin_project_member, @project) +- group = group_link.shared_with_group +- can_admin_member = local_assigns[:can_admin_member] +- group_link_path = local_assigns[:group_link_path] - dom_id = "group_member_#{group_link.id}" -# Note this is just for groups. For individual members please see shared/members/_member @@ -17,7 +18,7 @@ %span{ class: ('text-warning' if group_link.expires_soon?) } = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) } .controls.member-controls.align-items-center - = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do + = form_tag group_link_path, method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do = hidden_field_tag "group_link[group_access]", group_link.group_access .member-form-control.dropdown.mr-sm-2.d-sm-inline-block %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", @@ -39,7 +40,7 @@ = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input - if can_admin_member - = link_to project_group_link_path(@project, group_link), + = link_to group_link_path, method: :delete, data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, qa_selector: 'delete_group_access_link' }, class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/shared/members/_invite_group.html.haml index d413048ca10..27c930bcbb5 100644 --- a/app/views/projects/project_members/_new_project_group.html.haml +++ b/app/views/shared/members/_invite_group.html.haml @@ -1,13 +1,18 @@ +- access_levels = local_assigns[:access_levels] +- default_access_level = local_assigns[:default_access_level] +- submit_url = local_assigns[:submit_url] +- group_link_field = local_assigns[:group_link_field] +- group_access_field = local_assigns[:group_access_field] .row .col-sm-12 - = form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do + = form_tag submit_url, class: 'invite-group-form js-requires-input', method: :post do .form-group - = label_tag :link_group_id, _("Select a group to invite"), class: "label-bold" - = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp qa-group-select-field", required: true) + = label_tag group_link_field, _("Select a group to invite"), class: "label-bold" + = groups_select_tag(group_link_field, data: { skip_groups: @skip_groups }, class: 'input-clamp qa-group-select-field', required: true) .form-group - = label_tag :link_group_access, _("Max access level"), class: "label-bold" + = label_tag group_access_field, _("Max access level"), class: "label-bold" .select-wrapper - = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" + = select_tag group_access_field, options_for_select(access_levels, default_access_level), data: { qa_selector: 'group_access_field' }, class: "form-control select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - permissions_docs_path = help_page_path('user/permissions') diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/shared/members/_invite_member.html.haml index 149b0d6cddd..d3a1c85e285 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/shared/members/_invite_member.html.haml @@ -1,13 +1,18 @@ +- access_levels = local_assigns[:access_levels] +- default_access_level = local_assigns[:default_access_level] +- submit_url = local_assigns[:submit_url] +- can_import_members = local_assigns[:can_import_members?] +- import_path = local_assigns[:import_path] .row .col-sm-12 - = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| + = form_tag submit_url, class: 'invite-users-form', method: :post do .form-group = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold" - = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") + = users_select_tag(:user_ids, multiple: true, class: 'input-clamp qa-member-select-field', scope: :all, email_user: true, placeholder: 'Search for members to update or invite') .form-group = label_tag :access_level, _("Choose a role permission"), class: "label-bold" .select-wrapper - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" + = select_tag :access_level, options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - permissions_docs_path = help_page_path('user/permissions') @@ -18,6 +23,6 @@ = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - = f.submit _("Add to project"), class: "btn btn-success qa-add-member-button" - - if can_import_members? - = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") + = submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_member_button' } + - if can_import_members + = link_to _("Import"), import_path, class: "btn btn-default", title: _("Import members from another project") |