Commit 2c0579b4 authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch...

Merge branch '332846-move-user-administration-to-a-dropdown-button-to-match-the-behavior-of-member-s-listing' into 'master'

Move admin user actions into a dropdown

See merge request gitlab-org/gitlab!64648
parents 9efd64ab b4389e48
......@@ -5,6 +5,7 @@ import {
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
......@@ -21,6 +22,9 @@ export default {
GlDropdownDivider,
...Actions,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
user: {
type: Object,
......@@ -30,6 +34,11 @@ export default {
type: Object,
required: true,
},
showButtonLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
userActions() {
......@@ -56,6 +65,13 @@ export default {
userPaths() {
return generateUserPaths(this.paths, this.user.username);
},
editButtonAttrs() {
return {
'data-testid': 'edit',
icon: 'pencil-square',
href: this.userPaths.edit,
};
},
},
methods: {
isLdapAction(action) {
......@@ -70,51 +86,68 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-justify-content-end" :data-testid="`user-actions-${user.id}`">
<gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{
$options.i18n.edit
}}</gl-button>
<div
class="gl-display-flex gl-justify-content-end gl-my-n2 gl-mx-n2"
:data-testid="`user-actions-${user.id}`"
>
<div v-if="hasEditAction" class="gl-p-2">
<gl-button v-if="showButtonLabels" v-bind="editButtonAttrs">{{
$options.i18n.edit
}}</gl-button>
<gl-button
v-else
v-gl-tooltip="$options.i18n.edit"
v-bind="editButtonAttrs"
:aria-label="$options.i18n.edit"
/>
</div>
<gl-dropdown
v-if="hasDropdownActions"
data-testid="dropdown-toggle"
right
class="gl-ml-2"
icon="settings"
>
<gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header>
<div v-if="hasDropdownActions" class="gl-p-2">
<gl-dropdown
data-testid="dropdown-toggle"
right
:text="$options.i18n.userAdministration"
:text-sr-only="!showButtonLabels"
icon="settings"
data-qa-selector="user_actions_dropdown_toggle"
:data-qa-username="user.username"
>
<gl-dropdown-section-header>{{
$options.i18n.userAdministration
}}</gl-dropdown-section-header>
<template v-for="action in dropdownSafeActions">
<component
:is="getActionComponent(action)"
v-if="getActionComponent(action)"
:key="action"
:path="userPaths[action]"
:username="user.name"
:data-testid="action"
>
{{ $options.i18n[action] }}
</component>
<gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action">
{{ $options.i18n[action] }}
</gl-dropdown-item>
</template>
<template v-for="action in dropdownSafeActions">
<component
:is="getActionComponent(action)"
v-if="getActionComponent(action)"
:key="action"
:path="userPaths[action]"
:username="user.name"
:data-testid="action"
>
{{ $options.i18n[action] }}
</component>
<gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action">
{{ $options.i18n[action] }}
</gl-dropdown-item>
</template>
<gl-dropdown-divider v-if="hasDeleteActions" />
<gl-dropdown-divider v-if="hasDeleteActions" />
<template v-for="action in dropdownDeleteActions">
<component
:is="getActionComponent(action)"
v-if="getActionComponent(action)"
:key="action"
:paths="userPaths"
:username="user.name"
:oncall-schedules="user.oncallSchedules"
:data-testid="`delete-${action}`"
>
{{ $options.i18n[action] }}
</component>
</template>
</gl-dropdown>
<template v-for="action in dropdownDeleteActions">
<component
:is="getActionComponent(action)"
v-if="getActionComponent(action)"
:key="action"
:paths="userPaths"
:username="user.name"
:oncall-schedules="user.oncallSchedules"
:data-testid="`delete-${action}`"
>
{{ $options.i18n[action] }}
</component>
</template>
</gl-dropdown>
</div>
</div>
</template>
......@@ -6,7 +6,7 @@ export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
export const I18N_USER_ACTIONS = {
edit: __('Edit'),
settings: __('Settings'),
userAdministration: s__('AdminUsers|User administration'),
unlock: __('Unlock'),
block: s__('AdminUsers|Block'),
unblock: s__('AdminUsers|Unblock'),
......
......@@ -5,6 +5,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
import AdminUsersApp from './components/app.vue';
import ModalManager from './components/modals/user_modal_manager.vue';
import UserActions from './components/user_actions.vue';
import {
CONFIRM_DELETE_BUTTON_SELECTOR,
MODAL_TEXTS_CONTAINER_SELECTOR,
......@@ -17,26 +18,33 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
const initApp = (el, component, userPropKey, props = {}) => {
if (!el) {
return false;
}
const { users, paths } = el.dataset;
const { [userPropKey]: user, paths } = el.dataset;
return new Vue({
el,
apolloProvider,
render: (createElement) =>
createElement(AdminUsersApp, {
createElement(component, {
props: {
users: convertObjectPropsToCamelCase(JSON.parse(users), { deep: true }),
[userPropKey]: convertObjectPropsToCamelCase(JSON.parse(user), { deep: true }),
paths: convertObjectPropsToCamelCase(JSON.parse(paths)),
...props,
},
}),
});
};
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) =>
initApp(el, AdminUsersApp, 'users');
export const initAdminUserActions = (el = document.querySelector('#js-admin-user-actions')) =>
initApp(el, UserActions, 'user', { showButtonLabels: true });
export const initDeleteUserModals = () => {
const modalsMountElement = document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR);
......
import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
initAdminUserActions();
initDeleteUserModals();
initConfirmModal();
import { initExpiresAtField } from '~/access_tokens';
import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
initAdminUserActions();
initDeleteUserModals();
initExpiresAtField();
initConfirmModal();
import { initAdminUsersApp, initDeleteUserModals } from '~/admin/users';
import { initAdminUsersApp, initDeleteUserModals, initAdminUserActions } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
initAdminUsersApp();
initAdminUserActions();
initDeleteUserModals();
initConfirmModal();
......@@ -123,114 +123,10 @@ module UsersHelper
!user.confirmed?
end
def user_block_data(user, message)
{
path: block_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Block user %{username}?') % { username: sanitize_name(user.name) },
messageHtml: message,
okVariant: 'warning',
okTitle: s_('AdminUsers|Block')
}.to_json
}
end
def user_unblock_data(user)
{
path: unblock_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Unblock user %{username}?') % { username: sanitize_name(user.name) },
message: s_('AdminUsers|You can always block their account again if needed.'),
okVariant: 'info',
okTitle: s_('AdminUsers|Unblock')
}.to_json
}
end
def user_block_effects
header = tag.p s_('AdminUsers|Blocking user has the following effects:')
list = tag.ul do
concat tag.li s_('AdminUsers|User will not be able to login')
concat tag.li s_('AdminUsers|User will not be able to access git repositories')
concat tag.li s_('AdminUsers|Personal projects will be left')
concat tag.li s_('AdminUsers|Owned groups will be left')
end
header + list
end
def user_ban_data(user)
{
path: ban_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Ban user %{username}?') % { username: sanitize_name(user.name) },
message: s_('AdminUsers|You can unban their account in the future. Their data remains intact.'),
okVariant: 'warning',
okTitle: s_('AdminUsers|Ban')
}.to_json
}
end
def user_unban_data(user)
{
path: unban_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Unban %{username}?') % { username: sanitize_name(user.name) },
message: s_('AdminUsers|You ban their account in the future if necessary.'),
okVariant: 'info',
okTitle: s_('AdminUsers|Unban')
}.to_json
}
end
def user_ban_effects
header = tag.p s_('AdminUsers|Banning the user has the following effects:')
list = tag.ul do
concat tag.li s_('AdminUsers|User will be blocked')
end
link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: help_page_path("user/admin_area/moderate_users", anchor: "ban-a-user") }
info = tag.p s_('AdminUsers|Learn more about %{link_start}banned users.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
header + list + info
end
def ban_feature_available?
Feature.enabled?(:ban_user_feature_flag)
end
def user_deactivation_data(user, message)
{
path: deactivate_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Deactivate user %{username}?') % { username: sanitize_name(user.name) },
messageHtml: message,
okVariant: 'warning',
okTitle: s_('AdminUsers|Deactivate')
}.to_json
}
end
def user_activation_data(user)
{
path: activate_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Activate user %{username}?') % { username: sanitize_name(user.name) },
message: s_('AdminUsers|You can always deactivate their account again if needed.'),
okVariant: 'info',
okTitle: s_('AdminUsers|Activate')
}.to_json
}
end
def confirm_user_data(user)
message = if user.unconfirmed_email.present?
_('This user has an unconfirmed email address (%{email}). You may force a confirmation.') % { email: user.unconfirmed_email }
......@@ -259,22 +155,6 @@ module UsersHelper
}
end
def user_deactivation_effects
header = tag.p s_('AdminUsers|Deactivating a user has the following effects:')
list = tag.ul do
concat tag.li s_('AdminUsers|The user will be logged out')
concat tag.li s_('AdminUsers|The user will not be able to access git repositories')
concat tag.li s_('AdminUsers|The user will not be able to access the API')
concat tag.li s_('AdminUsers|The user will not receive any notifications')
concat tag.li s_('AdminUsers|The user will not be able to use slash commands')
concat tag.li s_('AdminUsers|When the user logs back in, their account will reactivate as a fully active account')
concat tag.li s_('AdminUsers|Personal projects, group and user history will be left intact')
end
header + list
end
def user_display_name(user)
return s_('UserProfile|Blocked user') if user.blocked?
......@@ -284,6 +164,13 @@ module UsersHelper
user.name
end
def admin_user_actions_data_attributes(user)
{
user: Admin::UserEntity.represent(user, { current_user: current_user }).to_json,
paths: admin_users_paths.to_json
}
end
private
def admin_users_paths
......
......@@ -15,3 +15,5 @@
= render @identities
- else
%h4= _('This user has no identities')
= render partial: 'admin/users/modals'
......@@ -28,3 +28,5 @@
impersonation: true,
active_tokens: @active_impersonation_tokens,
revoke_route_helper: ->(token) { revoke_admin_user_impersonation_token_path(token.user, token) }
= render partial: 'admin/users/modals'
.card.border-info
.card-header.gl-bg-blue-500.gl-text-white
= s_('AdminUsers|This user has requested access')
.card-body
= render partial: 'admin/users/user_approve_effects'
%br
= link_to s_('AdminUsers|Approve user'), approve_admin_user_path(user), method: :put, class: "btn gl-button btn-info", data: { confirm: s_('AdminUsers|Are you sure?'), qa_selector: 'approve_user_button' }
- if ban_feature_available?
.card.border-warning
.card-header.bg-warning.gl-text-white
= s_('AdminUsers|Ban user')
.card-body
= user_ban_effects
%br
%button.btn.gl-button.btn-warning.js-confirm-modal-button{ data: user_ban_data(user) }
= s_('AdminUsers|Ban user')
.card.border-warning
.card-header.bg-warning.text-white
= s_('AdminUsers|Block this user')
.card-body
= user_block_effects
%br
%button.btn.gl-button.btn-warning.js-confirm-modal-button{ data: user_block_data(user, s_('AdminUsers|You can always unblock their account, their data will remain intact.')) }
= s_('AdminUsers|Block user')
%h3.page-title
= @user.name
- if @user.blocked_pending_approval?
%span.cred
= s_('AdminUsers|(Pending approval)')
- elsif @user.banned?
%span.cred
= s_('AdminUsers|(Banned)')
- elsif @user.blocked?
%span.cred
= s_('AdminUsers|(Blocked)')
- if @user.internal?
%span.cred
= s_('AdminUsers|(Internal)')
- if @user.admin
%span.cred
= s_('AdminUsers|(Admin)')
- if @user.deactivated?
%span.cred
= s_('AdminUsers|(Deactivated)')
= render_if_exists 'admin/users/auditor_user_badge'
= render_if_exists 'admin/users/gma_user_badge'
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between.gl-align-items-center.gl-py-3.gl-mb-5.gl-border-b-solid.gl-border-gray-100.gl-border-b-1
.gl-my-3
%h3.page-title.gl-m-0
= @user.name
- if @user.blocked_pending_approval?
%span.cred
= s_('AdminUsers|(Pending approval)')
- elsif @user.banned?
%span.cred
= s_('AdminUsers|(Banned)')
- elsif @user.blocked?
%span.cred
= s_('AdminUsers|(Blocked)')
- if @user.internal?
%span.cred
= s_('AdminUsers|(Internal)')
- if @user.admin
%span.cred
= s_('AdminUsers|(Admin)')
- if @user.deactivated?
%span.cred
= s_('AdminUsers|(Deactivated)')
= render_if_exists 'admin/users/auditor_user_badge'
= render_if_exists 'admin/users/gma_user_badge'
.float-right
= link_to edit_admin_user_path(@user), class: "btn btn-default gl-button btn-grouped" do
= sprite_icon('pencil-square', css_class: 'gl-icon gl-button-icon')
= _('Edit')
.gl-my-3.gl-display-flex.gl-flex-wrap.gl-my-n2.gl-mx-n2
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
- if @user != current_user
- if impersonation_enabled? && @user.can?(:log_in)
= link_to _('Impersonate'), impersonate_admin_user_path(@user), method: :post, class: "btn btn-default gl-button btn-grouped", data: { qa_selector: 'impersonate_user_link' }
- if can_force_email_confirmation?(@user)
%button.btn.gl-button.btn-info.btn-grouped.js-confirm-modal-button{ data: confirm_user_data(@user) }
= _('Confirm user')
%hr
.gl-p-2
- if impersonation_enabled? && @user.can?(:log_in)
= link_to _('Impersonate'), impersonate_admin_user_path(@user), method: :post, class: "btn btn-default gl-button", data: { qa_selector: 'impersonate_user_link' }
- if can_force_email_confirmation?(@user)
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
= _('Confirm user')
%ul.nav-links.nav.nav-tabs
= nav_link(path: 'users#show') do
= link_to _("Account"), admin_user_path(@user)
......
.card.border-danger
.card-header.bg-danger.gl-text-white
= s_('AdminUsers|This user has requested access')
.card-body
= render partial: 'admin/users/user_reject_effects'
%br
= link_to s_('AdminUsers|Reject request'), reject_admin_user_path(user), method: :delete, class: "btn gl-button btn-danger", data: { confirm: s_('AdminUsers|Are you sure?') }
%p
= s_('AdminUsers|Reactivating a user will:')
%ul
%li
= s_('AdminUsers|Restore user access to the account, including web, Git and API.')
= render_if_exists 'admin/users/user_activation_effects_on_seats'
%p
= s_('AdminUsers|Approved users can:')
%ul
%li
= s_('AdminUsers|Log in')
%li
= s_('AdminUsers|Access Git repositories')
%li
= s_('AdminUsers|Access the API')
%li
= s_('AdminUsers|Be added to groups and projects')
- if @user.note.present?
- text = @user.note
.card.border-info
.card-header.bg-info.text-white
.card
.card-header
= _('Admin Note')
.card-body
%p= text
%p
= s_('AdminUsers|Rejected users:')
%ul
%li
= s_('AdminUsers|Cannot sign in or access instance information')
%li
= s_('AdminUsers|Will be deleted')
%p
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path("user/profile/account/delete_account", anchor: "associated-records") }
= s_('AdminUsers|For more information, please refer to the %{link_start}user account deletion documentation.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
......@@ -3,3 +3,4 @@
- page_title _("SSH Keys"), @user.name, _("Users")
= render 'admin/users/head'
= render 'profiles/keys/key_table', admin: true
= render partial: 'admin/users/modals'
......@@ -48,3 +48,5 @@
- if member.respond_to? :project
= link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from project') do
= sprite_icon('close', size: 16, css_class: 'gl-icon')
= render partial: 'admin/users/modals'
......@@ -16,8 +16,10 @@
%strong
= link_to user_path(@user) do
= @user.username
= render 'admin/users/profile', user: @user
-# Rendered on mobile only so order of cards can be different on desktop vs mobile
.gl-md-display-none
= render 'admin/users/profile', user: @user
= render 'admin/users/user_detail_note'
.card
.card-header
= _('Account:')
......@@ -139,112 +141,8 @@
= render 'shared/custom_attributes', custom_attributes: @user.custom_attributes
.col-md-6
- unless @user == current_user
= render 'admin/users/user_detail_note'
- unless @user.internal?
- if @user.deactivated?
.gl-card.border-info.gl-mb-5
.gl-card-header.bg-info.text-white
= _('Reactivate this user')
.gl-card-body
= render partial: 'admin/users/user_activation_effects'
%br
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_activation_data(@user) }
= s_('AdminUsers|Activate user')
- elsif @user.can_be_deactivated?
.gl-card.border-warning.gl-mb-5
.gl-card-header.bg-warning.text-white
= _('Deactivate this user')
.gl-card-body
= user_deactivation_effects
%br
%button.btn.gl-button.btn-warning.js-confirm-modal-button{ data: user_deactivation_data(@user, s_('AdminUsers|You can always re-activate their account, their data will remain intact.')) }
= s_('AdminUsers|Deactivate user')
- if @user.blocked?
- if @user.blocked_pending_approval?
= render 'admin/users/approve_user', user: @user
= render 'admin/users/reject_pending_user', user: @user
- elsif @user.banned?
.gl-card.border-info.gl-mb-5
.gl-card-header.gl-bg-blue-500.gl-text-white
= _('This user is banned')
.gl-card-body
%p= _('A banned user cannot:')
%ul
%li= _('Log in')
%li= _('Access Git repositories')
- link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: help_page_path("user/admin_area/moderate_users", anchor: "ban-a-user") }
= s_('AdminUsers|Learn more about %{link_start}banned users.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%p
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unban_data(@user) }
= s_('AdminUsers|Unban user')
- else
.gl-card.border-info.gl-mb-5
.gl-card-header.gl-bg-blue-500.gl-text-white
= _('This user is blocked')
.gl-card-body
%p= _('A blocked user cannot:')
%ul
%li= _('Log in')
%li= _('Access Git repositories')
%br
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unblock_data(@user) }
= s_('AdminUsers|Unblock user')
- elsif !@user.internal?
= render 'admin/users/block_user', user: @user
= render 'admin/users/ban_user', user: @user
- if @user.access_locked?
.card.border-info.gl-mb-5
.card-header.bg-info.text-white
= _('This account has been locked')
.card-body
%p= _('This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.')
%br
= link_to _('Unlock user'), unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?') }
- if !@user.blocked_pending_approval?
.gl-card.border-danger.gl-mb-5
.gl-card-header.bg-danger.text-white
= s_('AdminUsers|Delete user')
.gl-card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p= _('Deleting a user has the following effects:')
= render 'users/deletion_guidance', user: @user
%br
%button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
%p
= _('This user is currently an owner in these groups:')
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
= _('You must transfer ownership or delete these groups before you can delete this user.')
- else
%p
= _("You don't have access to delete this user.")
.gl-card.border-danger
.gl-card-header.bg-danger.text-white
= s_('AdminUsers|Delete user and contributions')
.gl-card-body
- if can?(current_user, :destroy_user, @user)
%p
- link_to_ghost_user = link_to(_("system ghost user"), help_page_path("user/profile/account/delete_account"))
= _("This option deletes the user and any contributions that would usually be moved to the %{link_to_ghost_user}. As well as the user's personal projects, groups owned solely by the user, and projects in them, will also be removed. Commits to other projects are unaffected.").html_safe % { link_to_ghost_user: link_to_ghost_user }
%br
%button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name } }
= s_('AdminUsers|Delete user and contributions')
- else
%p
= _("You don't have access to delete this user.")
-# Rendered on desktop only so order of cards can be different on desktop vs mobile
.col-md-6.gl-display-none.gl-md-display-block
= render 'admin/users/profile', user: @user
= render 'admin/users/user_detail_note'
= render partial: 'admin/users/modals'
......@@ -47,7 +47,8 @@ To approve or reject a user sign up:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select the **Pending approval** tab.
1. In the user's row, select settings (**{settings}**).
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Approve** or **Reject**.
Approving a user:
......
......@@ -23,8 +23,9 @@ or directly from the Admin Area. To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select a user.
1. Under the **Account** tab, select **Block user**.
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Block**.
A blocked user:
......@@ -47,8 +48,9 @@ A blocked user can be unblocked from the Admin Area. To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select on the **Blocked** tab.
1. Select a user.
1. Under the **Account** tab, select **Unblock user**.
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Unblock**.
Users can also be unblocked using the [GitLab API](../../api/users.md#unblock-user).
......@@ -85,8 +87,9 @@ A user can be deactivated from the Admin Area. To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select a user.
1. Under the **Account** tab, select **Deactivate user**.
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Deactivate**.
Please note that for the deactivation option to be visible to an admin, the user:
......@@ -126,8 +129,9 @@ To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select the **Deactivated** tab.
1. Select a user.
1. Under the **Account** tab, select **Activate user**.
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Activate**.
Users can also be activated using the [GitLab API](../../api/users.md#activate-user).
......@@ -157,8 +161,9 @@ Users can be banned using the Admin Area. To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select a user.
1. Under the **Account** tab, select **Ban user**.
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Ban user**.
NOTE:
This feature is a work in progress. Currently, banning a user
......@@ -172,8 +177,9 @@ A banned user can be unbanned using the Admin Area. To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select the **Banned** tab.
1. Select a user.
1. Under the **Account** tab, select **Unban user**.
1. (Optional) Select a user.
1. Select the **{settings}** **User administration** dropdown.
1. Select **Unban user**.
NOTE:
Unbanning a user changes the user's state to active and consumes a
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Changes GL.com plan for group' do
RSpec.describe 'Changes GL.com plan for group', :js do
include WaitForRequests
let!(:premium_plan) { create(:premium_plan) }
......
......@@ -1405,18 +1405,12 @@ msgstr ""
msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates."
msgstr ""
msgid "A banned user cannot:"
msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
msgid "A basic template for developing Linux programs using Kotlin Native"
msgstr ""
msgid "A blocked user cannot:"
msgstr ""
msgid "A complete DevOps platform"
msgstr ""
......@@ -1702,9 +1696,6 @@ msgstr ""
msgid "Acceptable for use in this project"
msgstr ""
msgid "Access Git repositories"
msgstr ""
msgid "Access Git repositories or the API."
msgstr ""
......@@ -2518,9 +2509,6 @@ msgstr ""
msgid "AdminUsers|Activate"
msgstr ""
msgid "AdminUsers|Activate user"
msgstr ""
msgid "AdminUsers|Activate user %{username}?"
msgstr ""
......@@ -2542,24 +2530,15 @@ msgstr ""
msgid "AdminUsers|Approve"
msgstr ""
msgid "AdminUsers|Approve user"
msgstr ""
msgid "AdminUsers|Approve user %{username}?"
msgstr ""
msgid "AdminUsers|Approved users can:"
msgstr ""
msgid "AdminUsers|Are you sure?"
msgstr ""
msgid "AdminUsers|Automatically marked as default internal user"
msgstr ""
msgid "AdminUsers|Ban"
msgstr ""
msgid "AdminUsers|Ban user"
msgstr ""
......@@ -2569,18 +2548,12 @@ msgstr ""
msgid "AdminUsers|Banned"
msgstr ""
msgid "AdminUsers|Banning the user has the following effects:"
msgstr ""
msgid "AdminUsers|Be added to groups and projects"
msgstr ""
msgid "AdminUsers|Block"
msgstr ""
msgid "AdminUsers|Block this user"
msgstr ""
msgid "AdminUsers|Block user"
msgstr ""
......@@ -2620,9 +2593,6 @@ msgstr ""
msgid "AdminUsers|Deactivate"
msgstr ""
msgid "AdminUsers|Deactivate user"
msgstr ""
msgid "AdminUsers|Deactivate user %{username}?"
msgstr ""
......@@ -2710,9 +2680,6 @@ msgstr ""
msgid "AdminUsers|Reject"
msgstr ""
msgid "AdminUsers|Reject request"
msgstr ""
msgid "AdminUsers|Reject user %{username}?"
msgstr ""
......@@ -2749,21 +2716,12 @@ msgstr ""
msgid "AdminUsers|The user will not receive any notifications"
msgstr ""
msgid "AdminUsers|This user has requested access"
msgstr ""
msgid "AdminUsers|To confirm, type %{projectName}"
msgstr ""
msgid "AdminUsers|To confirm, type %{username}"
msgstr ""
msgid "AdminUsers|Unban"
msgstr ""
msgid "AdminUsers|Unban %{username}?"
msgstr ""
msgid "AdminUsers|Unban user"
msgstr ""
......@@ -2773,19 +2731,16 @@ msgstr ""
msgid "AdminUsers|Unblock"
msgstr ""
msgid "AdminUsers|Unblock user"
msgstr ""
msgid "AdminUsers|Unblock user %{username}?"
msgstr ""
msgid "AdminUsers|Unlock user %{username}?"
msgstr ""
msgid "AdminUsers|User is validated and can use free CI minutes on shared runners."
msgid "AdminUsers|User administration"
msgstr ""
msgid "AdminUsers|User will be blocked"
msgid "AdminUsers|User is validated and can use free CI minutes on shared runners."
msgstr ""
msgid "AdminUsers|User will not be able to access git repositories"
......@@ -2830,9 +2785,6 @@ msgstr ""
msgid "AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd}, it cannot be undone or recovered."
msgstr ""
msgid "AdminUsers|You ban their account in the future if necessary."
msgstr ""
msgid "AdminUsers|You can always block their account again if needed."
msgstr ""
......@@ -10449,9 +10401,6 @@ msgstr ""
msgid "Deactivate dormant users after 90 days of inactivity. Users can return to active status by signing in to their account. While inactive, a user is not counted as an active user in the instance."
msgstr ""
msgid "Deactivate this user"
msgstr ""
msgid "Dear Administrator,"
msgstr ""
......@@ -10701,9 +10650,6 @@ msgstr ""
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Deleting a user has the following effects:"
msgstr ""
msgid "Deleting the project will delete its repository and all related resources, including issues and merge requests."
msgstr ""
......@@ -19868,9 +19814,6 @@ msgstr ""
msgid "Locks the discussion."
msgstr ""
msgid "Log in"
msgstr ""
msgid "Login with smartcard"
msgstr ""
......@@ -26921,9 +26864,6 @@ msgstr ""
msgid "Re-verification interval"
msgstr ""
msgid "Reactivate this user"
msgstr ""
msgid "Read documentation"
msgstr ""
......@@ -33360,9 +33300,6 @@ msgstr ""
msgid "This URL is already used for another link; duplicate URLs are not allowed"
msgstr ""
msgid "This account has been locked"
msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr ""
......@@ -33708,9 +33645,6 @@ msgstr ""
msgid "This only applies to repository indexing operations."
msgstr ""
msgid "This option deletes the user and any contributions that would usually be moved to the %{link_to_ghost_user}. As well as the user's personal projects, groups owned solely by the user, and projects in them, will also be removed. Commits to other projects are unaffected."
msgstr ""
msgid "This option is only available on GitLab.com"
msgstr ""
......@@ -33813,9 +33747,6 @@ msgstr ""
msgid "This user has an unconfirmed email address. You may force a confirmation."
msgstr ""
msgid "This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account."
msgstr ""
msgid "This user has no active %{type}."
msgstr ""
......@@ -33831,15 +33762,6 @@ msgstr ""
msgid "This user has the %{access} role in the %{name} project."
msgstr ""
msgid "This user is banned"
msgstr ""
msgid "This user is blocked"
msgstr ""
msgid "This user is currently an owner in these groups:"
msgstr ""
msgid "This user is the author of this %{noteable}."
msgstr ""
......@@ -35100,9 +35022,6 @@ msgstr ""
msgid "Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
msgstr ""
msgid "Unlock user"
msgstr ""
msgid "Unlocked"
msgstr ""
......@@ -37623,9 +37542,6 @@ msgstr ""
msgid "You do not have permissions to run the import."
msgstr ""
msgid "You don't have access to delete this user."
msgstr ""
msgid "You don't have any U2F devices registered yet."
msgstr ""
......@@ -37773,9 +37689,6 @@ msgstr ""
msgid "You must solve the CAPTCHA in order to submit"
msgstr ""
msgid "You must transfer ownership or delete these groups before you can delete this user."
msgstr ""
msgid "You must upload a file with the same file name when dropping onto an existing design."
msgstr ""
......@@ -39748,9 +39661,6 @@ msgstr ""
msgid "suggestPipeline|We’re adding a GitLab CI configuration file to add a pipeline to the project. You could create it manually, but we recommend that you start with a GitLab template that works out of the box."
msgstr ""
msgid "system ghost user"
msgstr ""
msgid "tag name"
msgstr ""
......
......@@ -14,8 +14,13 @@ module QA
element :user_id_content
end
view 'app/views/admin/users/_approve_user.html.haml' do
view 'app/assets/javascripts/admin/users/components/actions/approve.vue' do
element :approve_user_button
element :approve_user_confirm_button
end
view 'app/assets/javascripts/admin/users/components/user_actions.vue' do
element :user_actions_dropdown_toggle
end
view 'app/helpers/users_helper.rb' do
......@@ -23,6 +28,10 @@ module QA
element :confirm_user_confirm_button
end
def open_user_actions_dropdown(user)
click_element(:user_actions_dropdown_toggle, username: user.username)
end
def click_impersonate_user
click_element(:impersonate_user_link)
end
......@@ -36,10 +45,10 @@ module QA
click_element :confirm_user_confirm_button
end
def approve_user
accept_confirm do
click_element :approve_user_button
end
def approve_user(user)
open_user_actions_dropdown(user)
click_element :approve_user_button
click_element :approve_user_confirm_button
end
end
end
......
......@@ -143,7 +143,7 @@ module QA
Page::Admin::Overview::Users::Show.perform do |show|
user.id = show.user_id.to_i
show.approve_user
show.approve_user(user)
end
expect(page).to have_text('Successfully approved')
......
......@@ -4,6 +4,8 @@ require 'spec_helper'
# Test an operation that triggers background jobs requiring administrative rights
RSpec.describe 'Admin mode for workers', :request_store do
include Spec::Support::Helpers::Features::AdminUsersHelpers
let(:user) { create(:user) }
let(:user_to_delete) { create(:user) }
......@@ -37,7 +39,8 @@ RSpec.describe 'Admin mode for workers', :request_store do
it 'can delete user', :js do
visit admin_user_path(user_to_delete)
click_button 'Delete user'
click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
page.within '.modal-dialog' do
find("input[name='username']").send_keys(user_to_delete.name)
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Admin::Users::User' do
include Spec::Support::Helpers::Features::AdminUsersHelpers
let_it_be(:user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:current_user) { create(:admin) }
......@@ -12,15 +14,18 @@ RSpec.describe 'Admin::Users::User' do
end
describe 'GET /admin/users/:id' do
it 'has user info', :aggregate_failures do
it 'has user info', :js, :aggregate_failures do
visit admin_user_path(user)
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_content("ID: #{user.id}")
expect(page).to have_content("Namespace ID: #{user.namespace_id}")
expect(page).to have_button('Deactivate user')
expect(page).to have_button('Block user')
click_user_dropdown_toggle(user.id)
expect(page).to have_button('Block')
expect(page).to have_button('Deactivate')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
......@@ -29,9 +34,7 @@ RSpec.describe 'Admin::Users::User' do
it 'shows confirmation and allows blocking and unblocking', :js do
visit admin_user_path(user)
find('button', text: 'Block user').click
wait_for_requests
click_action_in_user_dropdown(user.id, 'Block')
expect(page).to have_content('Block user')
expect(page).to have_content('You can always unblock their account, their data will remain intact.')
......@@ -41,21 +44,18 @@ RSpec.describe 'Admin::Users::User' do
wait_for_requests
expect(page).to have_content('Successfully blocked')
expect(page).to have_content('This user is blocked')
find('button', text: 'Unblock user').click
wait_for_requests
click_action_in_user_dropdown(user.id, 'Unblock')
expect(page).to have_content('Unblock user')
expect(page).to have_content('You can always block their account again if needed.')
find('.modal-footer button', text: 'Unblock').click
wait_for_requests
expect(page).to have_content('Successfully unblocked')
expect(page).to have_content('Block this user')
click_user_dropdown_toggle(user.id)
expect(page).to have_content('Block')
end
end
......@@ -63,9 +63,7 @@ RSpec.describe 'Admin::Users::User' do
it 'shows confirmation and allows deactivating/re-activating', :js do
visit admin_user_path(user)
find('button', text: 'Deactivate user').click
wait_for_requests
click_action_in_user_dropdown(user.id, 'Deactivate')
expect(page).to have_content('Deactivate user')
expect(page).to have_content('You can always re-activate their account, their data will remain intact.')
......@@ -75,11 +73,8 @@ RSpec.describe 'Admin::Users::User' do
wait_for_requests
expect(page).to have_content('Successfully deactivated')
expect(page).to have_content('Reactivate this user')
find('button', text: 'Activate user').click
wait_for_requests
click_action_in_user_dropdown(user.id, 'Activate')
expect(page).to have_content('Activate user')
expect(page).to have_content('You can always deactivate their account again if needed.')
......@@ -89,7 +84,9 @@ RSpec.describe 'Admin::Users::User' do
wait_for_requests
expect(page).to have_content('Successfully activated')
expect(page).to have_content('Deactivate this user')
click_user_dropdown_toggle(user.id)
expect(page).to have_content('Deactivate')
end
end
......@@ -367,8 +364,11 @@ RSpec.describe 'Admin::Users::User' do
expect(page).to have_content(user.name)
expect(page).to have_content('Pending approval')
expect(page).to have_link('Approve user')
expect(page).to have_link('Reject request')
click_user_dropdown_toggle(user.id)
expect(page).to have_button('Approve')
expect(page).to have_button('Reject')
end
end
end
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Admin::Users' do
include Spec::Support::Helpers::Features::AdminUsersHelpers
let_it_be(:user, reload: true) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:current_user) { create(:admin) }
......@@ -572,12 +574,6 @@ RSpec.describe 'Admin::Users' do
end
end
def click_user_dropdown_toggle(user_id)
page.within("[data-testid='user-actions-#{user_id}']") do
find("[data-testid='dropdown-toggle']").click
end
end
def first_row
page.all('[role="row"]')[1]
end
......@@ -592,14 +588,4 @@ RSpec.describe 'Admin::Users' do
click_link option
end
end
def click_action_in_user_dropdown(user_id, action)
click_user_dropdown_toggle(user_id)
within find("[data-testid='user-actions-#{user_id}']") do
find('li button', text: action).click
end
wait_for_requests
end
end
import { GlDropdownDivider } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Actions from '~/admin/users/components/actions';
import AdminUserActions from '~/admin/users/components/user_actions.vue';
......@@ -20,7 +21,7 @@ describe('AdminUserActions component', () => {
findUserActions(id).find('[data-testid="dropdown-toggle"]');
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const initComponent = ({ actions = [] } = {}) => {
const initComponent = ({ actions = [], showButtonLabels } = {}) => {
wrapper = shallowMountExtended(AdminUserActions, {
propsData: {
user: {
......@@ -28,6 +29,10 @@ describe('AdminUserActions component', () => {
actions,
},
paths,
showButtonLabels,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
......@@ -144,4 +149,42 @@ describe('AdminUserActions component', () => {
});
});
});
describe('when `showButtonLabels` prop is `false`', () => {
beforeEach(() => {
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS] });
});
it('does not render "Edit" button label', () => {
const tooltip = getBinding(findEditButton().element, 'gl-tooltip');
expect(findEditButton().text()).toBe('');
expect(findEditButton().attributes('aria-label')).toBe(I18N_USER_ACTIONS.edit);
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit);
});
it('does not render "User administration" dropdown button label', () => {
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
expect(findActionsDropdown().props('textSrOnly')).toBe(true);
});
});
describe('when `showButtonLabels` prop is `true`', () => {
beforeEach(() => {
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS], showButtonLabels: true });
});
it('renders "Edit" button label', () => {
const tooltip = getBinding(findEditButton().element, 'gl-tooltip');
expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit);
expect(tooltip).not.toBeDefined();
});
it('renders "User administration" dropdown button label', () => {
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
expect(findActionsDropdown().props('textSrOnly')).toBe(false);
});
});
});
import { createWrapper } from '@vue/test-utils';
import { initAdminUsersApp } from '~/admin/users';
import { initAdminUsersApp, initAdminUserActions } from '~/admin/users';
import AdminUsersApp from '~/admin/users/components/app.vue';
import { users, paths } from './mock_data';
import UserActions from '~/admin/users/components/user_actions.vue';
import { users, user, paths } from './mock_data';
describe('initAdminUsersApp', () => {
let wrapper;
......@@ -14,15 +15,12 @@ describe('initAdminUsersApp', () => {
el.setAttribute('data-users', JSON.stringify(users));
el.setAttribute('data-paths', JSON.stringify(paths));
document.body.appendChild(el);
wrapper = createWrapper(initAdminUsersApp(el));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
el.remove();
el = null;
});
......@@ -33,3 +31,31 @@ describe('initAdminUsersApp', () => {
});
});
});
describe('initAdminUserActions', () => {
let wrapper;
let el;
const findUserActions = () => wrapper.find(UserActions);
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-user', JSON.stringify(user));
el.setAttribute('data-paths', JSON.stringify(paths));
wrapper = createWrapper(initAdminUserActions(el));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
el = null;
});
it('parses and passes props', () => {
expect(findUserActions().props()).toMatchObject({
user,
paths,
});
});
});
......@@ -18,6 +18,8 @@ export const users = [
},
];
export const user = users[0];
export const paths = {
edit: '/admin/users/id/edit',
approve: '/admin/users/id/approve',
......
......@@ -396,4 +396,22 @@ RSpec.describe UsersHelper do
end
end
end
describe '#admin_user_actions_data_attributes' do
subject(:data) { helper.admin_user_actions_data_attributes(user) }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(Admin::UserEntity).to receive(:represent).and_call_original
end
it 'user matches the serialized json' do
expect(data[:user]).to be_valid_json
expect(Admin::UserEntity).to have_received(:represent).with(user, hash_including({ current_user: user }))
end
it 'paths matches the schema' do
expect(data[:paths]).to match_schema('entities/admin_users_data_attributes_paths')
end
end
end
# frozen_string_literal: true
module Spec
module Support
module Helpers
module Features
module AdminUsersHelpers
def click_user_dropdown_toggle(user_id)
page.within("[data-testid='user-actions-#{user_id}']") do
find("[data-testid='dropdown-toggle']").click
end
end
def click_action_in_user_dropdown(user_id, action)
click_user_dropdown_toggle(user_id)
within find("[data-testid='user-actions-#{user_id}']") do
find('li button', exact_text: action).click
end
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment