summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-13 15:07:53 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-13 15:07:53 +0000
commita5ab3467a705b62911feacc3cf627fdbb00aa198 (patch)
tree65143ce13405efccb922fc428624ad57c38b6efa /app
parenteb30dd6e28f6fc9eb8021d205f6ed84511f001e2 (diff)
downloadgitlab-ce-a5ab3467a705b62911feacc3cf627fdbb00aa198.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index/index.js3
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue84
-rw-r--r--app/assets/javascripts/registry/settings/store/getters.js8
-rw-r--r--app/assets/javascripts/registry/settings/store/index.js2
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js5
-rw-r--r--app/assets/javascripts/registry/settings/store/state.js4
-rw-r--r--app/assets/javascripts/registry/settings/utils.js6
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js26
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb65
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb2
-rw-r--r--app/controllers/groups/group_links_controller.rb24
-rw-r--r--app/controllers/groups/group_members_controller.rb41
-rw-r--r--app/finders/group_members_finder.rb44
-rw-r--r--app/graphql/mutations/snippets/create.rb2
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/groups/group_members_helper.rb4
-rw-r--r--app/helpers/selects_helper.rb3
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/group_group_link.rb4
-rw-r--r--app/models/project_group_link.rb2
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml3
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml22
-rw-r--r--app/views/groups/group_members/index.html.haml42
-rw-r--r--app/views/projects/project_members/_groups.html.haml4
-rw-r--r--app/views/projects/project_members/_team.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml8
-rw-r--r--app/views/projects/registry/settings/_index.haml5
-rw-r--r--app/views/shared/members/_group.html.haml9
-rw-r--r--app/views/shared/members/_invite_group.html.haml (renamed from app/views/projects/project_members/_new_project_group.html.haml)15
-rw-r--r--app/views/shared/members/_invite_member.html.haml (renamed from app/views/projects/project_members/_new_project_member.html.haml)17
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")