Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-06 09:10:31 +00:00
parent 92bd840b61
commit b3c8b65ec2
59 changed files with 610 additions and 196 deletions

View File

@ -17,17 +17,18 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@skip_groups += @project.group.self_and_ancestors_ids if @project.group
@group_links = @project.project_group_links
@group_links = @group_links.search(params[:search]) if params[:search].present?
@group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present?
@project_members = MembersFinder
project_members = MembersFinder
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
@project_members = present_members(@project_members.page(params[:page]))
if helpers.can_manage_project_members?(@project)
@invited_members = present_members(project_members.invite)
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
end
@requesters = present_members(
AccessRequestsFinder.new(@project).execute(current_user)
)
@project_members = present_members(project_members.non_invite.page(params[:page]))
@project_member = @project.project_members.new
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Projects::ProjectMembersHelper
def can_manage_project_members?(project)
can?(current_user, :admin_project_member, project)
end
def show_groups?(group_links)
group_links.exists? || groups_tab_active?
end
def show_invited_members?(project, invited_members)
can_manage_project_members?(project) && invited_members.exists?
end
def show_access_requests?(project, requesters)
can_manage_project_members?(project) && requesters.exists?
end
def groups_tab_active?
params[:search_groups].present?
end
def current_user_is_group_owner?(project)
return false if project.group.nil?
project.group.has_owner?(current_user)
end
end

View File

@ -53,18 +53,18 @@
#tab-members.tab-pane{ class: ('active' unless invited_active) }
.card.card-without-border
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= render 'shared/members/search_field'
- if can_manage_members
= render 'groups/group_members/tab_pane/form_item' do
= render 'shared/members/tab_pane/form_item' do
= label_tag '2fa', _('2FA'), class: form_item_label_css_class
= render 'shared/members/filter_2fa_dropdown'
= render 'groups/group_members/tab_pane/form_item' do
= render 'shared/members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
@ -75,8 +75,8 @@
#tab-groups.tab-pane
.card.card-without-border
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
@ -85,8 +85,8 @@
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
= render 'shared/members/search_field', name: 'search_invited'
@ -98,8 +98,8 @@
#tab-access-requests.tab-pane
.card.card-without-border
- unless filtered_search_enabled
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading

View File

@ -1,8 +1,11 @@
.card.project-members-groups
.card-header
= html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%span.badge.badge-pill= group_links.size
%ul.content-list.members-list
- can_admin_member = can?(current_user, :admin_project_member, @project)
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
= form_tag project_project_members_path(@project), method: :get, class: 'user-search-form gl-mx-n3 gl-my-n3', data: { testid: 'group-link-search-form' } do
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= render 'shared/members/search_field', name: 'search_groups'
%ul.content-list.members-list{ data: { testid: 'project-member-groups' } }
- @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)
= render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_project_members?(@project), group_link_path: project_group_link_path(@project, group_link)

View File

@ -1,20 +1,18 @@
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
- group = local_assigns.fetch(:group)
- current_user_is_group_owner = group && group.has_owner?(current_user)
- current_user_is_group_owner = local_assigns.fetch(:current_user_is_group_owner)
.card
.card-header.flex-project-members-panel
%span.flex-project-title
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_("Members of %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%span.badge.badge-pill= members.total_count
= form_tag project_project_members_path(project), method: :get, class: 'form-inline user-search-form flex-users-form' do
.form-group
.position-relative
= search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false }
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= sprite_icon('search', css_class: 'gl-vertical-align-middle!')
= label_tag :sort_by, _('Sort by'), class: 'col-form-label label-bold px-2'
= form_tag project_project_members_path(project), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
.gl-px-3.gl-py-2
.search-control-wrap.gl-relative
= render 'shared/members/search_field'
= render 'shared/members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list{ data: { qa_selector: 'members_list', testid: 'members-table' } }
= render partial: 'shared/members/member',

View File

@ -1,5 +1,4 @@
- page_title _("Members")
- can_admin_project_members = can?(current_user, :admin_project_member, @project)
- group = @project.group
.js-remove-member-modal
@ -8,37 +7,73 @@
- if project_can_be_shared?
%h4
= _("Project members")
- if can_admin_project_members
- if can_manage_project_members?(@project)
%p= share_project_description(@project)
- else
%p
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
.light
- if can_admin_project_members && project_can_be_shared?
- if !membership_locked? && @project.allowed_to_share_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', class: ('active' if membership_locked?) }
%a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
- if can_manage_project_members?(@project) && project_can_be_shared?
- if !membership_locked? && @project.allowed_to_share_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', class: ('active' if membership_locked?) }
%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 '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 '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 '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 '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, group: group, requesters: @requesters
.clearfix
%h5.member.existing-title
= _("Existing members and groups")
- if @group_links.any?
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= 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 '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 '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 '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'
%ul.nav-links.mobile-separator.nav.nav-tabs
%li.nav-item
= link_to '#tab-members', class: ['nav-link', ('active' unless groups_tab_active?)], data: { toggle: 'tab' } do
%span
= _('Members')
%span.badge.badge-pill= @project_members.total_count
- if show_groups?(@group_links)
%li.nav-item
= link_to '#tab-groups', class: ['nav-link', ('active' if groups_tab_active?)] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
%span
= _('Groups')
%span.badge.badge-pill= @group_links.count
- if show_invited_members?(@project, @invited_members)
%li.nav-item
= link_to '#tab-invited-members', class: 'nav-link', data: { toggle: 'tab' } do
%span
= _('Invited')
%span.badge.badge-pill= @invited_members.count
- if show_access_requests?(@project, @requesters)
%li.nav-item
= link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do
%span
= _('Access requests')
%span.badge.badge-pill= @requesters.count
.tab-content
#tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
= render 'projects/project_members/team', project: @project, group: group, members: @project_members, current_user_is_group_owner: current_user_is_group_owner?(@project)
= paginate @project_members, theme: "gitlab", params: { search_groups: nil }
- if show_groups?(@group_links)
#tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
= render 'projects/project_members/groups', group_links: @group_links
= render 'projects/project_members/team', project: @project, group: group, members: @project_members
= paginate @project_members, theme: "gitlab"
- if show_invited_members?(@project, @invited_members)
#tab-invited-members.tab-pane
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Members invited to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @invited_members, as: :member, locals: { membership_source: @project, group: group, current_user_is_group_owner: current_user_is_group_owner?(@project) }
- if show_access_requests?(@project, @requesters)
#tab-access-requests.tab-pane
.card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @requesters, as: :member, locals: { membership_source: @project, group: group }

View File

@ -0,0 +1,5 @@
---
title: Reorganize project member management into tabs
merge_request: 49764
author:
type: changed

View File

@ -516,12 +516,14 @@ class MyMigration < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
INDEX_NAME = 'index_name'
def up
add_concurrent_index :table, :column
add_concurrent_index :table, :column, name: INDEX_NAME
end
def down
remove_concurrent_index :table, :column, name: index_name
remove_concurrent_index :table, :column, name: INDEX_NAME
end
end
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

View File

@ -21,7 +21,7 @@ project's **Members**.
When your project belongs to the group, group members inherit the membership and permission
level for the project from the group.
![Project members page](img/project_members.png)
![Project members page](img/project_members_13_8.png)
From the image above, we can deduce the following things:
@ -46,17 +46,17 @@ using the dropdown on the right side:
Right next to **People**, start typing the name or username of the user you
want to add.
![Search for people](img/add_user_search_people.png)
![Search for people](img/add_user_search_people_13_8.png)
Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
![Give user permissions](img/add_user_give_permissions_13_8.png)
Once done, select **Add users to project** and they are immediately added to
your project with the permissions you gave them above.
![List members](img/add_user_list_members.png)
![List members](img/add_user_list_members_13_8.png)
From there on, you can either remove an existing user or change their access
level to the project.
@ -68,14 +68,14 @@ You can import another project's users in your own project by hitting the
In the dropdown menu, you can see only the projects you are Maintainer on.
![Import members from another project](img/add_user_import_members_from_another_project.png)
![Import members from another project](img/add_user_import_members_from_another_project_13_8.png)
Select the one you want and hit **Import project members**. A flash message
displays, notifying you that the import was successful, and the new members
are now in the project's members list. Notice that the permissions that they
had on the project you imported from are retained.
![Members list of new members](img/add_user_imported_members.png)
![Members list of new members](img/add_user_imported_members_13_8.png)
## Invite people using their e-mail address
@ -83,18 +83,18 @@ If a user you want to give access to doesn't have an account on your GitLab
instance, you can invite them just by typing their e-mail address in the
user search field.
![Invite user by mail](img/add_user_email_search.png)
![Invite user by mail](img/add_user_email_search_13_8.png)
As you can imagine, you can mix inviting multiple people and adding existing
GitLab users to the project.
![Invite user by mail ready to submit](img/add_user_email_ready.png)
![Invite user by mail ready to submit](img/add_user_email_ready_13_8.png)
Once done, hit **Add users to project** and watch that there is a new member
with the e-mail address we used above. From there on, you can resend the
invitation, change their access level, or even delete them.
![Invite user members list](img/add_user_email_accept.png)
![Invite user members list](img/add_user_email_accept_13_8.png)
While unaccepted, the system automatically sends reminder emails on the second, fifth,
and tenth day after the invitation was initially sent.
@ -130,7 +130,7 @@ NOTE:
If a project does not have any maintainers, the notification is sent to the
most recently active owners of the project's group.
![Manage access requests](img/access_requests_management.png)
![Manage access requests](img/access_requests_management_13_8.png)
If you change your mind before your request is approved, just click the
**Withdraw Access Request** button.

View File

@ -26,19 +26,20 @@ To share 'Project Acme' with the 'Engineering' group:
1. For 'Project Acme' use the left navigation menu to go to **Members**.
![share project with groups](img/share_project_with_groups_tab_v13_6.png)
![share project with groups](img/share_project_with_groups_tab_v13_8.png)
1. Select the **Invite group** tab.
1. Add the 'Engineering' group with the maximum access level of your choice.
1. Optionally, select an expiring date.
1. Click **Invite**.
1. After sharing 'Project Acme' with 'Engineering':
- The group is listed in the **Groups** tab.
![share project with groups tab](img/share_project_with_groups_tab_v13_6.png)
!['Engineering' group is listed in Groups tab](img/project_groups_tab_13_8.png)
1. After sharing 'Project Acme' with 'Engineering', the project is listed
on the group dashboard
- The project is listed on the group dashboard.
!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project_v13_6.png)
!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project_v13_8.png)
Note that you can only share a project with:

View File

@ -111,8 +111,8 @@ module Gitlab
private
def before_send(event, hint)
event = add_context_from_exception_type(event, hint)
event = custom_fingerprinting(event, hint)
inject_context_for_exception(event, hint[:exception])
custom_fingerprinting(event, hint[:exception])
event
end
@ -123,7 +123,6 @@ module Gitlab
end
extra = sanitize_request_parameters(extra)
inject_sql_query_into_extra(exception, extra)
if sentry && Raven.configuration.server
Raven.capture_exception(exception, tags: default_tags, extra: extra)
@ -150,12 +149,6 @@ module Gitlab
filter.filter(parameters)
end
def inject_sql_query_into_extra(exception, extra)
return unless exception.is_a?(ActiveRecord::StatementInvalid)
extra[:sql] = PgQuery.normalize(exception.sql.to_s)
end
def sentry_dsn
return unless Rails.env.production? || Rails.env.development?
return unless Gitlab.config.sentry.enabled
@ -183,9 +176,17 @@ module Gitlab
{}
end
# Debugging for https://gitlab.com/gitlab-org/gitlab-foss/issues/57727
def add_context_from_exception_type(event, hint)
if ActiveModel::MissingAttributeError === hint[:exception]
# Group common, mostly non-actionable exceptions by type and message,
# rather than cause
def custom_fingerprinting(event, ex)
return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name)
event.fingerprint = [ex.class.name, ex.message]
end
def inject_context_for_exception(event, ex)
case ex
when ActiveModel::MissingAttributeError # Debugging for https://gitlab.com/gitlab-org/gitlab/-/issues/26751
columns_hash = ActiveRecord::Base
.connection
.schema_cache
@ -193,21 +194,11 @@ module Gitlab
.transform_values { |v| v.map(&:first) }
event.extra.merge!(columns_hash)
when ActiveRecord::StatementInvalid
event.extra[:sql] = PgQuery.normalize(ex.sql.to_s)
else
inject_context_for_exception(event, ex.cause) if ex.cause.present?
end
event
end
# Group common, mostly non-actionable exceptions by type and message,
# rather than cause
def custom_fingerprinting(event, hint)
ex = hint[:exception]
return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name)
event.fingerprint = [ex.class.name, ex.message]
event
end
end
end

View File

@ -11559,9 +11559,6 @@ msgstr ""
msgid "Existing branch name, tag, or commit SHA"
msgstr ""
msgid "Existing members and groups"
msgstr ""
msgid "Existing projects may be moved into a group"
msgstr ""
@ -12428,9 +12425,6 @@ msgstr ""
msgid "Find by path"
msgstr ""
msgid "Find existing members by name"
msgstr ""
msgid "Find file"
msgstr ""
@ -17272,6 +17266,9 @@ msgstr ""
msgid "Members invited to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Members invited to %{strong_start}%{project_name}%{strong_end}"
msgstr ""
msgid "Members listed as CODEOWNERS of affected files."
msgstr ""
@ -30733,6 +30730,9 @@ msgstr ""
msgid "Users requesting access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Users requesting access to %{strong_start}%{project_name}%{strong_end}"
msgstr ""
msgid "Users were successfully added."
msgstr ""

View File

@ -42,9 +42,9 @@
"@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.177.0",
"@gitlab/svgs": "1.178.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "25.2.1",
"@gitlab/ui": "25.3.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",

View File

@ -17,6 +17,7 @@ module QA
view 'app/views/projects/project_members/index.html.haml' do
element :invite_group_tab
element :groups_list_tab
end
view 'app/views/shared/members/_invite_group.html.haml' do
@ -48,6 +49,7 @@ module QA
def remove_group(group_name)
click_element :invite_group_tab
click_element :groups_list_tab
page.accept_alert do
within_element(:group_row, text: group_name) do
click_element :delete_group_access_link

View File

@ -14,32 +14,137 @@ RSpec.describe Projects::ProjectMembersController do
expect(response).to have_gitlab_http_status(:ok)
end
context 'when project belongs to group' do
let(:user_in_group) { create(:user) }
let(:project_in_group) { create(:project, :public, group: group) }
context 'project members' do
context 'when project belongs to group' do
let(:user_in_group) { create(:user) }
let(:project_in_group) { create(:project, :public, group: group) }
before do
group.add_owner(user_in_group)
project_in_group.add_maintainer(user)
sign_in(user)
end
it 'lists inherited project members by default' do
get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
end
it 'lists direct project members only' do
get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
end
it 'lists inherited project members only' do
get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
end
end
context 'when invited members are present' do
let!(:invited_member) { create(:project_member, :invited, project: project) }
before do
project.add_maintainer(user)
sign_in(user)
end
it 'excludes the invited members from project members list' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email)
end
end
end
context 'group links' do
let!(:project_group_link) { create(:project_group_link, project: project, group: group) }
it 'lists group links' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(assigns(:group_links).map(&:id)).to contain_exactly(project_group_link.id)
end
context 'when `search_groups` param is present' do
let(:group_2) { create(:group, :public, name: 'group_2') }
let!(:project_group_link_2) { create(:project_group_link, project: project, group: group_2) }
it 'lists group links that match search' do
get :index, params: { namespace_id: project.namespace, project_id: project, search_groups: 'group_2' }
expect(assigns(:group_links).map(&:id)).to contain_exactly(project_group_link_2.id)
end
end
end
context 'invited members' do
let!(:invited_member) { create(:project_member, :invited, project: project) }
before do
group.add_owner(user_in_group)
project_in_group.add_maintainer(user)
project.add_maintainer(user)
sign_in(user)
end
it 'lists inherited project members by default' do
get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
context 'when user has `admin_project_member` permissions' do
before do
allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(true)
end
expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
it 'lists invited members' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(assigns(:invited_members).map(&:invite_email)).to contain_exactly(invited_member.invite_email)
end
end
it 'lists direct project members only' do
get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
context 'when user does not have `admin_project_member` permissions' do
before do
allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(false)
end
expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
it 'does not list invited members' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(assigns(:invited_members)).to be_nil
end
end
end
context 'access requests' do
let(:access_requester_user) { create(:user) }
before do
project.request_access(access_requester_user)
project.add_maintainer(user)
sign_in(user)
end
it 'lists inherited project members only' do
get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
context 'when user has `admin_project_member` permissions' do
before do
allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(true)
end
expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
it 'lists access requests' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(assigns(:requesters).map(&:user_id)).to contain_exactly(access_requester_user.id)
end
end
context 'when user does not have `admin_project_member` permissions' do
before do
allow(controller.helpers).to receive(:can_manage_project_members?).with(project).and_return(false)
end
it 'does not list access requests' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(assigns(:requesters)).to be_nil
end
end
end
end

View File

@ -3,7 +3,7 @@
FactoryBot.define do
factory :project_group_link do
project
group
group { association(:group) }
expires_at { nil }
group_access { Gitlab::Access::DEVELOPER }

View File

@ -15,7 +15,9 @@ FactoryBot.define do
trait(:invited) do
user_id { nil }
invite_token { 'xxx' }
invite_email { 'email@email.com' }
sequence :invite_email do |n|
"email#{n}@email.com"
end
end
trait :blocked do

View File

@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Groups > Members > Maintainer manages access requests' do
it_behaves_like 'Maintainer manages access requests' do
let(:has_tabs) { true }
let(:entity) { create(:group, :public) }
let(:members_page_path) { group_group_members_path(entity) }
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects members' do
RSpec.describe 'Projects members', :js do
let(:user) { create(:user) }
let(:developer) { create(:user) }
let(:group) { create(:group, :public) }
@ -66,26 +66,47 @@ RSpec.describe 'Projects members' do
end
end
context 'with a group and a project invitee' do
context 'with a group, a project invitee, and a project requester' do
before do
group.request_access(group_requester)
project.request_access(project_requester)
group_invitee
project_invitee
visit project_project_members_path(project)
end
it 'shows the project invitee, the project developer, and the group owner' do
it 'shows the group owner' do
page.within first('.content-list') do
expect(page).to have_content('test1@abc.com')
expect(page).not_to have_content('test2@abc.com')
# Project developer
expect(page).to have_content(developer.name)
# Group owner
expect(page).to have_content(user.name)
expect(page).to have_content(group.name)
end
end
it 'shows the project developer' do
page.within first('.content-list') do
# Project developer
expect(page).to have_content(developer.name)
end
end
it 'shows the project invitee' do
click_link 'Invited'
page.within first('.content-list') do
expect(page).to have_content('test1@abc.com')
expect(page).not_to have_content('test2@abc.com')
end
end
it 'shows the project requester' do
click_link 'Access requests'
page.within first('.content-list') do
expect(page).to have_content(project_requester.name)
expect(page).not_to have_content(group_requester.name)
end
end
end
context 'with a group requester' do
@ -95,36 +116,13 @@ RSpec.describe 'Projects members' do
end
it 'does not appear in the project members page' do
expect(page).not_to have_link('Access requests')
page.within first('.content-list') do
expect(page).not_to have_content(group_requester.name)
end
end
end
context 'with a group and a project requesters' do
before do
group.request_access(group_requester)
project.request_access(project_requester)
visit project_project_members_path(project)
end
it 'shows the project requester, the project developer, and the group owner' do
page.within first('.content-list') do
expect(page).to have_content(project_requester.name)
expect(page).not_to have_content(group_requester.name)
end
page.within all('.content-list').last do
# Project developer
expect(page).to have_content(developer.name)
# Group owner
expect(page).to have_content(user.name)
expect(page).to have_content(group.name)
end
end
end
describe 'showing status of members' do
it_behaves_like 'showing user status' do
let(:user_with_status) { developer }

View File

@ -16,6 +16,7 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
project.add_maintainer(user)
sign_in(user)
visit project_project_members_path(project)
click_groups_tab
end
it 'updates group access level' do
@ -29,6 +30,8 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
visit project_project_members_path(project)
click_groups_tab
expect(first('.group_member')).to have_content('Guest')
end
@ -71,23 +74,31 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
expect(page).not_to have_selector('.group_member')
end
context 'search in existing members (yes, this filters the groups list as well)' do
context 'search in existing members' do
it 'finds no results' do
page.within '.user-search-form' do
fill_in 'search', with: 'testing 123'
fill_in 'search_groups', with: 'testing 123'
find('.user-search-btn').click
end
click_groups_tab
expect(page).not_to have_selector('.group_member')
end
it 'finds results' do
page.within '.user-search-form' do
fill_in 'search', with: group.name
fill_in 'search_groups', with: group.name
find('.user-search-btn').click
end
click_groups_tab
expect(page).to have_selector('.group_member', count: 1)
end
end
def click_groups_tab
click_link 'Groups'
end
end

View File

@ -39,7 +39,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
it 'the project can be shared with another group' do
visit project_project_members_path(project)
expect(page).not_to have_css('.project-members-groups')
expect(page).not_to have_link 'Groups'
click_on 'invite-group-tab'
@ -47,7 +47,9 @@ RSpec.describe 'Project > Members > Invite group', :js do
page.find('body').click
find('.btn-success').click
page.within('.project-members-groups') do
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content(group_to_share_with.name)
end
end
@ -132,7 +134,9 @@ RSpec.describe 'Project > Members > Invite group', :js do
end
it 'the group link shows the expiration time with a warning class' do
page.within('.project-members-groups') do
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)

View File

@ -82,7 +82,9 @@ RSpec.describe 'Project members list' do
add_user('test@example.com', 'Reporter')
page.within(second_row) do
click_link 'Invited'
page.within(first_row) do
expect(page).to have_content('test@example.com')
expect(page).to have_content('Invited')
expect(page).to have_button('Reporter')

View File

@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Projects > Members > Maintainer manages access requests' do
it_behaves_like 'Maintainer manages access requests' do
let(:has_tabs) { false }
let(:entity) { create(:project, :public) }
let(:members_page_path) { project_project_members_path(entity) }
end

View File

@ -0,0 +1,73 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects > Members > Tabs' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, creator: user, namespace: user.namespace) }
let_it_be(:group) { create(:group) }
let_it_be(:project_members) { create_list(:project_member, 2, project: project) }
let_it_be(:access_requests) { create_list(:project_member, 2, :access_request, project: project) }
let_it_be(:invites) { create_list(:project_member, 2, :invited, project: project) }
let_it_be(:project_group_links) { create_list(:project_group_link, 2, project: project) }
shared_examples 'active "Members" tab' do
it 'displays "Members" tab' do
expect(page).to have_selector('.nav-link.active', text: 'Members')
end
end
before do
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
sign_in(user)
visit project_project_members_path(project)
end
where(:tab, :count) do
'Members' | 3
'Invited' | 2
'Groups' | 2
'Access requests' | 2
end
with_them do
it "renders #{params[:tab]} tab" do
expect(page).to have_selector('.nav-link', text: "#{tab} #{count}")
end
end
context 'displays "Members" tab by default' do
it_behaves_like 'active "Members" tab'
end
context 'when searching "Groups"', :js do
before do
click_link 'Groups'
page.within '[data-testid="group-link-search-form"]' do
fill_in 'search_groups', with: 'group'
find('button[type="submit"]').click
end
end
it 'displays "Groups" tab' do
expect(page).to have_selector('.nav-link.active', text: 'Groups')
end
context 'and then searching "Members"' do
before do
click_link 'Members 3'
page.within '[data-testid="user-search-form"]' do
fill_in 'search', with: 'user'
find('button[type="submit"]').click
end
end
it_behaves_like 'active "Members" tab'
end
end
end

View File

@ -57,7 +57,7 @@ RSpec.describe 'Projects > Settings > User manages project members' do
end
end
it 'shows all members of project shared group' do
it 'shows all members of project shared group', :js do
group.add_owner(user)
group.add_developer(user_dmitriy)
@ -67,7 +67,9 @@ RSpec.describe 'Projects > Settings > User manages project members' do
visit(project_project_members_path(project))
page.within('.project-members-groups') do
click_link 'Groups'
page.within('[data-testid="project-member-groups"]') do
expect(page).to have_content('OpenSource')
expect(first('.group_member')).to have_content('Maintainer')
end

View File

@ -19,7 +19,7 @@ describe('Submit Changes Error', () => {
});
};
const findRetryButton = () => wrapper.findAll(GlButton).at(1);
const findRetryButton = () => wrapper.find(GlButton);
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ProjectMembersHelper do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:allow_admin_project) { nil }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).with(current_user, :admin_project_member, project).and_return(allow_admin_project)
end
shared_examples 'when `current_user` does not have `admin_project_member` permissions' do
let(:allow_admin_project) { false }
it { is_expected.to be(false) }
end
describe '#can_manage_project_members?' do
subject { helper.can_manage_project_members?(project) }
context 'when `current_user` has `admin_project_member` permissions' do
let(:allow_admin_project) { true }
it { is_expected.to be(true) }
end
include_examples 'when `current_user` does not have `admin_project_member` permissions'
end
describe '#show_groups?' do
subject { helper.show_groups?(project.project_group_links) }
context 'when group links exist' do
let!(:project_group_link) { create(:project_group_link, project: project) }
it { is_expected.to be(true) }
end
context 'when `search_groups` param is set' do
before do
allow(helper).to receive(:params).and_return({ search_groups: 'foo' })
end
it { is_expected.to be(true) }
end
context 'when `search_groups` param is not set and group links do not exist' do
it { is_expected.to be(false) }
end
end
describe '#show_invited_members?' do
subject { helper.show_invited_members?(project, project.project_members.invite) }
context 'when `current_user` has `admin_project_member` permissions' do
let(:allow_admin_project) { true }
context 'when invited members exist' do
let!(:invite) { create(:project_member, :invited, project: project) }
it { is_expected.to be(true) }
end
context 'when invited members do not exist' do
it { is_expected.to be(false) }
end
end
include_examples 'when `current_user` does not have `admin_project_member` permissions'
end
describe '#show_access_requests?' do
subject { helper.show_access_requests?(project, project.requesters) }
context 'when `current_user` has `admin_project_member` permissions' do
let(:allow_admin_project) { true }
context 'when access requests exist' do
let!(:access_request) { create(:project_member, :access_request, project: project) }
it { is_expected.to be(true) }
end
context 'when access requests do not exist' do
it { is_expected.to be(false) }
end
end
include_examples 'when `current_user` does not have `admin_project_member` permissions'
end
describe '#groups_tab_active?' do
subject { helper.groups_tab_active? }
context 'when `search_groups` param is set' do
before do
allow(helper).to receive(:params).and_return({ search_groups: 'foo' })
end
it { is_expected.to be(true) }
end
context 'when `search_groups` param is not set' do
it { is_expected.to be(false) }
end
end
describe '#current_user_is_group_owner?' do
let(:group) { create(:group) }
subject { helper.current_user_is_group_owner?(project2) }
describe "when current user is the owner of the project's parent group" do
let(:project2) { create(:project, namespace: group) }
before do
group.add_owner(current_user)
end
it { is_expected.to be(true) }
end
describe "when current user is not the owner of the project's parent group" do
let_it_be(:user) { create(:user) }
let(:project2) { create(:project, namespace: group) }
before do
group.add_owner(user)
end
it { is_expected.to be(false) }
end
describe "when project does not have a parent group" do
let(:user) { create(:user) }
let(:project2) { create(:project, namespace: user.namespace) }
it { is_expected.to be(false) }
end
end
end

View File

@ -236,7 +236,7 @@ RSpec.describe Gitlab::ErrorTracking do
context 'the exception implements :sentry_extra_data' do
let(:extra_info) { { event: 'explosion', size: :massive } }
let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller) }
let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller, cause: nil) }
it 'includes the extra data from the exception in the tracking information' do
track_exception
@ -247,7 +247,7 @@ RSpec.describe Gitlab::ErrorTracking do
end
context 'the exception implements :sentry_extra_data, which returns nil' do
let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller) }
let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller, cause: nil) }
let(:extra) { { issue_url: issue_url } }
it 'just includes the other extra info' do
@ -287,10 +287,23 @@ RSpec.describe Gitlab::ErrorTracking do
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') }
it 'injects the normalized sql query into extra' do
track_exception
allow(Raven.client.transport).to receive(:send_event) do |event|
expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
end
expect(Raven).to have_received(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')))
track_exception
end
end
context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do
let(:exception) { RuntimeError.new(cause: ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1')) }
it 'injects the normalized sql query into extra' do
allow(Raven.client.transport).to receive(:send_event) do |event|
expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
end
track_exception
end
end
end

View File

@ -12,9 +12,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
sign_in(maintainer)
visit members_page_path
if has_tabs
click_on 'Access requests'
end
click_on 'Access requests'
end
it 'maintainer can see access requests', :js do
@ -48,11 +46,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
end
def expect_visible_access_request(entity, user)
if has_tabs
expect(page).to have_content "Access requests 1"
else
expect(page).to have_content "Users requesting access to #{entity.name} 1"
end
expect(page).to have_content "Access requests 1"
expect(page).to have_content user.name
end

View File

@ -861,20 +861,20 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
"@gitlab/svgs@1.177.0":
version "1.177.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.177.0.tgz#e481ed327a11d3834c8b1668d7485b9eefef97f5"
integrity sha512-L7DggusgkbubNFCRIYtCuYiLx+t5Hp8y/XIxJ3RM5mqAfxkTR1KxALNLDP9CT7xWieHDhNvgcXAdamGoi0ofDQ==
"@gitlab/svgs@1.178.0":
version "1.178.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.178.0.tgz#069edb8abb4c7137d48f527592476655f066538b"
integrity sha512-m1xe5SPgpi9lSFCHHTkkGeScxkqhi7aD8qApL5F4MqCGeKF9IhELIVoMD1R6vkfjzFJh0BwFREPkuwjnAOMKfA==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@25.2.1":
version "25.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.2.1.tgz#2c332134bbc82a6c40ff5fdb73aacccf730629d8"
integrity sha512-bOkL2sfkovCV6MO/N70Xfe+vTdyi2Vp2efgDvOx4tHzqJllM6Y379wculi0VmdGw3X4TpmPI+zLWAAZ9vkhDAQ==
"@gitlab/ui@25.3.1":
version "25.3.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.3.1.tgz#7557c810397f8c4b81c5360e4642afc3f8274dfc"
integrity sha512-vCl74UZgQ5m1caJk8O067KKYa+DP40ES2XDnM/wAc9mZAMynP0GPpePc3cmTLY8vpfzxx2A2iJr04SLgI2pxjA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"