Commit f77a237d authored by peterhegman's avatar peterhegman Committed by Jacques Erasmus

Move admin user actions from cards to a dropdown

Remove user action cards in admin area and move the actions to a
"User administration" dropdown.

Changelog: changed
parent f9e527a1
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownDivider, GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { convertArrayToCamelCase } from '~/lib/utils/common_utils'; import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
...@@ -21,6 +22,9 @@ export default { ...@@ -21,6 +22,9 @@ export default {
GlDropdownDivider, GlDropdownDivider,
...Actions, ...Actions,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
user: { user: {
type: Object, type: Object,
...@@ -30,6 +34,11 @@ export default { ...@@ -30,6 +34,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
showButtonLabels: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
userActions() { userActions() {
...@@ -56,6 +65,13 @@ export default { ...@@ -56,6 +65,13 @@ export default {
userPaths() { userPaths() {
return generateUserPaths(this.paths, this.user.username); return generateUserPaths(this.paths, this.user.username);
}, },
editButtonAttrs() {
return {
'data-testid': 'edit',
icon: 'pencil-square',
href: this.userPaths.edit,
};
},
}, },
methods: { methods: {
isLdapAction(action) { isLdapAction(action) {
...@@ -70,51 +86,68 @@ export default { ...@@ -70,51 +86,68 @@ export default {
</script> </script>
<template> <template>
<div class="gl-display-flex gl-justify-content-end" :data-testid="`user-actions-${user.id}`"> <div
<gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{ class="gl-display-flex gl-justify-content-end gl-my-n2 gl-mx-n2"
$options.i18n.edit :data-testid="`user-actions-${user.id}`"
}}</gl-button> >
<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 <div v-if="hasDropdownActions" class="gl-p-2">
v-if="hasDropdownActions" <gl-dropdown
data-testid="dropdown-toggle" data-testid="dropdown-toggle"
right right
class="gl-ml-2" :text="$options.i18n.userAdministration"
icon="settings" :text-sr-only="!showButtonLabels"
> icon="settings"
<gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header> data-qa-selector="user_actions_dropdown_toggle"
:data-qa-index="user.id"
>
<gl-dropdown-section-header>{{
$options.i18n.userAdministration
}}</gl-dropdown-section-header>
<template v-for="action in dropdownSafeActions"> <template v-for="action in dropdownSafeActions">
<component <component
:is="getActionComponent(action)" :is="getActionComponent(action)"
v-if="getActionComponent(action)" v-if="getActionComponent(action)"
:key="action" :key="action"
:path="userPaths[action]" :path="userPaths[action]"
:username="user.name" :username="user.name"
:data-testid="action" :data-testid="action"
> >
{{ $options.i18n[action] }} {{ $options.i18n[action] }}
</component> </component>
<gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action"> <gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action">
{{ $options.i18n[action] }} {{ $options.i18n[action] }}
</gl-dropdown-item> </gl-dropdown-item>
</template> </template>
<gl-dropdown-divider v-if="hasDeleteActions" /> <gl-dropdown-divider v-if="hasDeleteActions" />
<template v-for="action in dropdownDeleteActions"> <template v-for="action in dropdownDeleteActions">
<component <component
:is="getActionComponent(action)" :is="getActionComponent(action)"
v-if="getActionComponent(action)" v-if="getActionComponent(action)"
:key="action" :key="action"
:paths="userPaths" :paths="userPaths"
:username="user.name" :username="user.name"
:oncall-schedules="user.oncallSchedules" :oncall-schedules="user.oncallSchedules"
:data-testid="`delete-${action}`" :data-testid="`delete-${action}`"
> >
{{ $options.i18n[action] }} {{ $options.i18n[action] }}
</component> </component>
</template> </template>
</gl-dropdown> </gl-dropdown>
</div>
</div> </div>
</template> </template>
...@@ -6,7 +6,7 @@ export const LENGTH_OF_USER_NOTE_TOOLTIP = 100; ...@@ -6,7 +6,7 @@ export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
export const I18N_USER_ACTIONS = { export const I18N_USER_ACTIONS = {
edit: __('Edit'), edit: __('Edit'),
settings: __('Settings'), userAdministration: s__('AdminUsers|User administration'),
unlock: __('Unlock'), unlock: __('Unlock'),
block: s__('AdminUsers|Block'), block: s__('AdminUsers|Block'),
unblock: s__('AdminUsers|Unblock'), unblock: s__('AdminUsers|Unblock'),
......
...@@ -5,6 +5,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; ...@@ -5,6 +5,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import AdminUsersApp from './components/app.vue'; import AdminUsersApp from './components/app.vue';
import ModalManager from './components/modals/user_modal_manager.vue'; import ModalManager from './components/modals/user_modal_manager.vue';
import UserActions from './components/user_actions.vue';
import { import {
CONFIRM_DELETE_BUTTON_SELECTOR, CONFIRM_DELETE_BUTTON_SELECTOR,
MODAL_TEXTS_CONTAINER_SELECTOR, MODAL_TEXTS_CONTAINER_SELECTOR,
...@@ -17,26 +18,33 @@ const apolloProvider = new VueApollo({ ...@@ -17,26 +18,33 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
}); });
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => { const initApp = (el, component, userPropKey, props = {}) => {
if (!el) { if (!el) {
return false; return false;
} }
const { users, paths } = el.dataset; const { [userPropKey]: user, paths } = el.dataset;
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
render: (createElement) => render: (createElement) =>
createElement(AdminUsersApp, { createElement(component, {
props: { props: {
users: convertObjectPropsToCamelCase(JSON.parse(users), { deep: true }), [userPropKey]: convertObjectPropsToCamelCase(JSON.parse(user), { deep: true }),
paths: convertObjectPropsToCamelCase(JSON.parse(paths)), 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 = () => { export const initDeleteUserModals = () => {
const modalsMountElement = document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR); const modalsMountElement = document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR);
......
import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
import initConfirmModal from '~/confirm_modal'; import initConfirmModal from '~/confirm_modal';
initAdminUserActions();
initDeleteUserModals();
initConfirmModal(); initConfirmModal();
import { initExpiresAtField } from '~/access_tokens'; import { initExpiresAtField } from '~/access_tokens';
import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
import initConfirmModal from '~/confirm_modal'; import initConfirmModal from '~/confirm_modal';
initAdminUserActions();
initDeleteUserModals();
initExpiresAtField(); initExpiresAtField();
initConfirmModal(); initConfirmModal();
import { initAdminUsersApp, initDeleteUserModals } from '~/admin/users'; import { initAdminUsersApp, initDeleteUserModals, initAdminUserActions } from '~/admin/users';
import initConfirmModal from '~/confirm_modal'; import initConfirmModal from '~/confirm_modal';
initAdminUsersApp(); initAdminUsersApp();
initAdminUserActions();
initDeleteUserModals(); initDeleteUserModals();
initConfirmModal(); initConfirmModal();
...@@ -123,114 +123,10 @@ module UsersHelper ...@@ -123,114 +123,10 @@ module UsersHelper
!user.confirmed? !user.confirmed?
end 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? def ban_feature_available?
Feature.enabled?(:ban_user_feature_flag) Feature.enabled?(:ban_user_feature_flag)
end 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) def confirm_user_data(user)
message = if user.unconfirmed_email.present? message = if user.unconfirmed_email.present?
_('This user has an unconfirmed email address (%{email}). You may force a confirmation.') % { email: user.unconfirmed_email } _('This user has an unconfirmed email address (%{email}). You may force a confirmation.') % { email: user.unconfirmed_email }
...@@ -259,22 +155,6 @@ module UsersHelper ...@@ -259,22 +155,6 @@ module UsersHelper
} }
end 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) def user_display_name(user)
return s_('UserProfile|Blocked user') if user.blocked? return s_('UserProfile|Blocked user') if user.blocked?
...@@ -284,6 +164,13 @@ module UsersHelper ...@@ -284,6 +164,13 @@ module UsersHelper
user.name user.name
end 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 private
def admin_users_paths def admin_users_paths
......
...@@ -15,3 +15,5 @@ ...@@ -15,3 +15,5 @@
= render @identities = render @identities
- else - else
%h4= _('This user has no identities') %h4= _('This user has no identities')
= render partial: 'admin/users/modals'
...@@ -28,3 +28,5 @@ ...@@ -28,3 +28,5 @@
impersonation: true, impersonation: true,
active_tokens: @active_impersonation_tokens, active_tokens: @active_impersonation_tokens,
revoke_route_helper: ->(token) { revoke_admin_user_impersonation_token_path(token.user, token) } 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 .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
= @user.name .gl-my-3
- if @user.blocked_pending_approval? %h3.page-title.gl-m-0
%span.cred = @user.name
= s_('AdminUsers|(Pending approval)') - if @user.blocked_pending_approval?
- elsif @user.banned? %span.cred
%span.cred = s_('AdminUsers|(Pending approval)')
= s_('AdminUsers|(Banned)') - elsif @user.banned?
- elsif @user.blocked? %span.cred
%span.cred = s_('AdminUsers|(Banned)')
= s_('AdminUsers|(Blocked)') - elsif @user.blocked?
- if @user.internal? %span.cred
%span.cred = s_('AdminUsers|(Blocked)')
= s_('AdminUsers|(Internal)') - if @user.internal?
- if @user.admin %span.cred
%span.cred = s_('AdminUsers|(Internal)')
= s_('AdminUsers|(Admin)') - if @user.admin
- if @user.deactivated? %span.cred
%span.cred = s_('AdminUsers|(Admin)')
= s_('AdminUsers|(Deactivated)') - if @user.deactivated?
= render_if_exists 'admin/users/auditor_user_badge' %span.cred
= render_if_exists 'admin/users/gma_user_badge' = s_('AdminUsers|(Deactivated)')
= render_if_exists 'admin/users/auditor_user_badge'
= render_if_exists 'admin/users/gma_user_badge'
.float-right .gl-my-3.gl-display-flex.gl-flex-wrap.gl-my-n2.gl-mx-n2
= link_to edit_admin_user_path(@user), class: "btn btn-default gl-button btn-grouped" do .gl-p-2
= sprite_icon('pencil-square', css_class: 'gl-icon gl-button-icon') #js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
= _('Edit')
- if @user != current_user - if @user != current_user
- if impersonation_enabled? && @user.can?(:log_in) .gl-p-2
= 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 impersonation_enabled? && @user.can?(:log_in)
- if can_force_email_confirmation?(@user) = link_to _('Impersonate'), impersonate_admin_user_path(@user), method: :post, class: "btn btn-default gl-button", data: { qa_selector: 'impersonate_user_link' }
%button.btn.gl-button.btn-info.btn-grouped.js-confirm-modal-button{ data: confirm_user_data(@user) } - if can_force_email_confirmation?(@user)
= _('Confirm user') %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
= _('Confirm user')
%hr
%ul.nav-links.nav.nav-tabs %ul.nav-links.nav.nav-tabs
= nav_link(path: 'users#show') do = nav_link(path: 'users#show') do
= link_to _("Account"), admin_user_path(@user) = 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? - if @user.note.present?
- text = @user.note - text = @user.note
.card.border-info .card
.card-header.bg-info.text-white .card-header
= _('Admin Note') = _('Admin Note')
.card-body .card-body
%p= text %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 @@ ...@@ -3,3 +3,4 @@
- page_title _("SSH Keys"), @user.name, _("Users") - page_title _("SSH Keys"), @user.name, _("Users")
= render 'admin/users/head' = render 'admin/users/head'
= render 'profiles/keys/key_table', admin: true = render 'profiles/keys/key_table', admin: true
= render partial: 'admin/users/modals'
...@@ -48,3 +48,5 @@ ...@@ -48,3 +48,5 @@
- if member.respond_to? :project - 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 = 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') = sprite_icon('close', size: 16, css_class: 'gl-icon')
= render partial: 'admin/users/modals'
...@@ -16,8 +16,10 @@ ...@@ -16,8 +16,10 @@
%strong %strong
= link_to user_path(@user) do = link_to user_path(@user) do
= @user.username = @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
.card-header .card-header
= _('Account:') = _('Account:')
...@@ -139,112 +141,8 @@ ...@@ -139,112 +141,8 @@
= render 'shared/custom_attributes', custom_attributes: @user.custom_attributes = render 'shared/custom_attributes', custom_attributes: @user.custom_attributes
.col-md-6 -# Rendered on desktop only so order of cards can be different on desktop vs mobile
- unless @user == current_user .col-md-6.gl-display-none.gl-md-display-block
= render 'admin/users/user_detail_note' = render 'admin/users/profile', user: @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.")
= render partial: 'admin/users/modals' = render partial: 'admin/users/modals'
...@@ -47,7 +47,8 @@ To approve or reject a user sign up: ...@@ -47,7 +47,8 @@ To approve or reject a user sign up:
1. On the top bar, select **Menu >** **{admin}** **Admin**. 1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select the **Pending approval** tab. 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**. 1. Select **Approve** or **Reject**.
Approving a user: Approving a user:
......
...@@ -23,8 +23,9 @@ or directly from the Admin Area. To do this: ...@@ -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 top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select a user. 1. (Optional) Select a user.
1. Under the **Account** tab, select **Block user**. 1. Select the **{settings}** **User administration** dropdown.
1. Select **Block**.
A blocked user: A blocked user:
...@@ -47,8 +48,9 @@ A blocked user can be unblocked from the Admin Area. To do this: ...@@ -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 top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select on the **Blocked** tab. 1. Select on the **Blocked** tab.
1. Select a user. 1. (Optional) Select a user.
1. Under the **Account** tab, select **Unblock 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). 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: ...@@ -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 top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select a user. 1. (Optional) Select a user.
1. Under the **Account** tab, select **Deactivate 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: Please note that for the deactivation option to be visible to an admin, the user:
...@@ -126,8 +129,9 @@ To do this: ...@@ -126,8 +129,9 @@ To do this:
1. On the top bar, select **Menu >** **{admin}** **Admin**. 1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select the **Deactivated** tab. 1. Select the **Deactivated** tab.
1. Select a user. 1. (Optional) Select a user.
1. Under the **Account** tab, select **Activate 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). 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: ...@@ -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 top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select a user. 1. (Optional) Select a user.
1. Under the **Account** tab, select **Ban user**. 1. Select the **{settings}** **User administration** dropdown.
1. Select **Ban user**.
NOTE: NOTE:
This feature is a work in progress. Currently, banning a user 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: ...@@ -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 top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Overview > Users**. 1. On the left sidebar, select **Overview > Users**.
1. Select the **Banned** tab. 1. Select the **Banned** tab.
1. Select a user. 1. (Optional) Select a user.
1. Under the **Account** tab, select **Unban user**. 1. Select the **{settings}** **User administration** dropdown.
1. Select **Unban user**.
NOTE: NOTE:
Unbanning a user changes the user's state to active and consumes a Unbanning a user changes the user's state to active and consumes a
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Changes GL.com plan for group' do RSpec.describe 'Changes GL.com plan for group', :js do
include WaitForRequests include WaitForRequests
let!(:premium_plan) { create(:premium_plan) } let!(:premium_plan) { create(:premium_plan) }
......
...@@ -1405,18 +1405,12 @@ msgstr "" ...@@ -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." 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 "" msgstr ""
msgid "A banned user cannot:"
msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages" msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr "" msgstr ""
msgid "A basic template for developing Linux programs using Kotlin Native" msgid "A basic template for developing Linux programs using Kotlin Native"
msgstr "" msgstr ""
msgid "A blocked user cannot:"
msgstr ""
msgid "A complete DevOps platform" msgid "A complete DevOps platform"
msgstr "" msgstr ""
...@@ -1702,9 +1696,6 @@ msgstr "" ...@@ -1702,9 +1696,6 @@ msgstr ""
msgid "Acceptable for use in this project" msgid "Acceptable for use in this project"
msgstr "" msgstr ""
msgid "Access Git repositories"
msgstr ""
msgid "Access Git repositories or the API." msgid "Access Git repositories or the API."
msgstr "" msgstr ""
...@@ -2518,9 +2509,6 @@ msgstr "" ...@@ -2518,9 +2509,6 @@ msgstr ""
msgid "AdminUsers|Activate" msgid "AdminUsers|Activate"
msgstr "" msgstr ""
msgid "AdminUsers|Activate user"
msgstr ""
msgid "AdminUsers|Activate user %{username}?" msgid "AdminUsers|Activate user %{username}?"
msgstr "" msgstr ""
...@@ -2542,24 +2530,15 @@ msgstr "" ...@@ -2542,24 +2530,15 @@ msgstr ""
msgid "AdminUsers|Approve" msgid "AdminUsers|Approve"
msgstr "" msgstr ""
msgid "AdminUsers|Approve user"
msgstr ""
msgid "AdminUsers|Approve user %{username}?" msgid "AdminUsers|Approve user %{username}?"
msgstr "" msgstr ""
msgid "AdminUsers|Approved users can:" msgid "AdminUsers|Approved users can:"
msgstr "" msgstr ""
msgid "AdminUsers|Are you sure?"
msgstr ""
msgid "AdminUsers|Automatically marked as default internal user" msgid "AdminUsers|Automatically marked as default internal user"
msgstr "" msgstr ""
msgid "AdminUsers|Ban"
msgstr ""
msgid "AdminUsers|Ban user" msgid "AdminUsers|Ban user"
msgstr "" msgstr ""
...@@ -2569,18 +2548,12 @@ msgstr "" ...@@ -2569,18 +2548,12 @@ msgstr ""
msgid "AdminUsers|Banned" msgid "AdminUsers|Banned"
msgstr "" msgstr ""
msgid "AdminUsers|Banning the user has the following effects:"
msgstr ""
msgid "AdminUsers|Be added to groups and projects" msgid "AdminUsers|Be added to groups and projects"
msgstr "" msgstr ""
msgid "AdminUsers|Block" msgid "AdminUsers|Block"
msgstr "" msgstr ""
msgid "AdminUsers|Block this user"
msgstr ""
msgid "AdminUsers|Block user" msgid "AdminUsers|Block user"
msgstr "" msgstr ""
...@@ -2620,9 +2593,6 @@ msgstr "" ...@@ -2620,9 +2593,6 @@ msgstr ""
msgid "AdminUsers|Deactivate" msgid "AdminUsers|Deactivate"
msgstr "" msgstr ""
msgid "AdminUsers|Deactivate user"
msgstr ""
msgid "AdminUsers|Deactivate user %{username}?" msgid "AdminUsers|Deactivate user %{username}?"
msgstr "" msgstr ""
...@@ -2710,9 +2680,6 @@ msgstr "" ...@@ -2710,9 +2680,6 @@ msgstr ""
msgid "AdminUsers|Reject" msgid "AdminUsers|Reject"
msgstr "" msgstr ""
msgid "AdminUsers|Reject request"
msgstr ""
msgid "AdminUsers|Reject user %{username}?" msgid "AdminUsers|Reject user %{username}?"
msgstr "" msgstr ""
...@@ -2749,21 +2716,12 @@ msgstr "" ...@@ -2749,21 +2716,12 @@ msgstr ""
msgid "AdminUsers|The user will not receive any notifications" msgid "AdminUsers|The user will not receive any notifications"
msgstr "" msgstr ""
msgid "AdminUsers|This user has requested access"
msgstr ""
msgid "AdminUsers|To confirm, type %{projectName}" msgid "AdminUsers|To confirm, type %{projectName}"
msgstr "" msgstr ""
msgid "AdminUsers|To confirm, type %{username}" msgid "AdminUsers|To confirm, type %{username}"
msgstr "" msgstr ""
msgid "AdminUsers|Unban"
msgstr ""
msgid "AdminUsers|Unban %{username}?"
msgstr ""
msgid "AdminUsers|Unban user" msgid "AdminUsers|Unban user"
msgstr "" msgstr ""
...@@ -2773,19 +2731,16 @@ msgstr "" ...@@ -2773,19 +2731,16 @@ msgstr ""
msgid "AdminUsers|Unblock" msgid "AdminUsers|Unblock"
msgstr "" msgstr ""
msgid "AdminUsers|Unblock user"
msgstr ""
msgid "AdminUsers|Unblock user %{username}?" msgid "AdminUsers|Unblock user %{username}?"
msgstr "" msgstr ""
msgid "AdminUsers|Unlock user %{username}?" msgid "AdminUsers|Unlock user %{username}?"
msgstr "" msgstr ""
msgid "AdminUsers|User is validated and can use free CI minutes on shared runners." msgid "AdminUsers|User administration"
msgstr "" msgstr ""
msgid "AdminUsers|User will be blocked" msgid "AdminUsers|User is validated and can use free CI minutes on shared runners."
msgstr "" msgstr ""
msgid "AdminUsers|User will not be able to access git repositories" msgid "AdminUsers|User will not be able to access git repositories"
...@@ -2830,9 +2785,6 @@ msgstr "" ...@@ -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." 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 "" msgstr ""
msgid "AdminUsers|You ban their account in the future if necessary."
msgstr ""
msgid "AdminUsers|You can always block their account again if needed." msgid "AdminUsers|You can always block their account again if needed."
msgstr "" msgstr ""
...@@ -10449,9 +10401,6 @@ 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." 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 "" msgstr ""
msgid "Deactivate this user"
msgstr ""
msgid "Dear Administrator," msgid "Dear Administrator,"
msgstr "" msgstr ""
...@@ -10701,9 +10650,6 @@ 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?" 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 "" 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." msgid "Deleting the project will delete its repository and all related resources, including issues and merge requests."
msgstr "" msgstr ""
...@@ -19868,9 +19814,6 @@ msgstr "" ...@@ -19868,9 +19814,6 @@ msgstr ""
msgid "Locks the discussion." msgid "Locks the discussion."
msgstr "" msgstr ""
msgid "Log in"
msgstr ""
msgid "Login with smartcard" msgid "Login with smartcard"
msgstr "" msgstr ""
...@@ -26921,9 +26864,6 @@ msgstr "" ...@@ -26921,9 +26864,6 @@ msgstr ""
msgid "Re-verification interval" msgid "Re-verification interval"
msgstr "" msgstr ""
msgid "Reactivate this user"
msgstr ""
msgid "Read documentation" msgid "Read documentation"
msgstr "" msgstr ""
...@@ -33360,9 +33300,6 @@ msgstr "" ...@@ -33360,9 +33300,6 @@ msgstr ""
msgid "This URL is already used for another link; duplicate URLs are not allowed" msgid "This URL is already used for another link; duplicate URLs are not allowed"
msgstr "" 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." msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr "" msgstr ""
...@@ -33708,9 +33645,6 @@ msgstr "" ...@@ -33708,9 +33645,6 @@ msgstr ""
msgid "This only applies to repository indexing operations." msgid "This only applies to repository indexing operations."
msgstr "" 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" msgid "This option is only available on GitLab.com"
msgstr "" msgstr ""
...@@ -33813,9 +33747,6 @@ msgstr "" ...@@ -33813,9 +33747,6 @@ msgstr ""
msgid "This user has an unconfirmed email address. You may force a confirmation." msgid "This user has an unconfirmed email address. You may force a confirmation."
msgstr "" 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}." msgid "This user has no active %{type}."
msgstr "" msgstr ""
...@@ -33831,15 +33762,6 @@ msgstr "" ...@@ -33831,15 +33762,6 @@ msgstr ""
msgid "This user has the %{access} role in the %{name} project." msgid "This user has the %{access} role in the %{name} project."
msgstr "" 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}." msgid "This user is the author of this %{noteable}."
msgstr "" msgstr ""
...@@ -35100,9 +35022,6 @@ msgstr "" ...@@ -35100,9 +35022,6 @@ msgstr ""
msgid "Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment." msgid "Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
msgstr "" msgstr ""
msgid "Unlock user"
msgstr ""
msgid "Unlocked" msgid "Unlocked"
msgstr "" msgstr ""
...@@ -37623,9 +37542,6 @@ msgstr "" ...@@ -37623,9 +37542,6 @@ msgstr ""
msgid "You do not have permissions to run the import." msgid "You do not have permissions to run the import."
msgstr "" msgstr ""
msgid "You don't have access to delete this user."
msgstr ""
msgid "You don't have any U2F devices registered yet." msgid "You don't have any U2F devices registered yet."
msgstr "" msgstr ""
...@@ -37773,9 +37689,6 @@ msgstr "" ...@@ -37773,9 +37689,6 @@ msgstr ""
msgid "You must solve the CAPTCHA in order to submit" msgid "You must solve the CAPTCHA in order to submit"
msgstr "" 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." msgid "You must upload a file with the same file name when dropping onto an existing design."
msgstr "" msgstr ""
...@@ -39748,9 +39661,6 @@ 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." 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 "" msgstr ""
msgid "system ghost user"
msgstr ""
msgid "tag name" msgid "tag name"
msgstr "" msgstr ""
......
...@@ -14,8 +14,13 @@ module QA ...@@ -14,8 +14,13 @@ module QA
element :user_id_content element :user_id_content
end 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_button
element :approve_user_confirm_button
end
view 'app/assets/javascripts/admin/users/components/user_actions.vue' do
element :user_actions_dropdown_toggle
end end
view 'app/helpers/users_helper.rb' do view 'app/helpers/users_helper.rb' do
...@@ -23,6 +28,10 @@ module QA ...@@ -23,6 +28,10 @@ module QA
element :confirm_user_confirm_button element :confirm_user_confirm_button
end end
def open_user_actions_dropdown(user)
click_element(:user_actions_dropdown_toggle, index: user.id)
end
def click_impersonate_user def click_impersonate_user
click_element(:impersonate_user_link) click_element(:impersonate_user_link)
end end
...@@ -36,10 +45,10 @@ module QA ...@@ -36,10 +45,10 @@ module QA
click_element :confirm_user_confirm_button click_element :confirm_user_confirm_button
end end
def approve_user def approve_user(user)
accept_confirm do open_user_actions_dropdown(user)
click_element :approve_user_button click_element :approve_user_button
end click_element :approve_user_confirm_button
end end
end end
end end
......
...@@ -143,7 +143,7 @@ module QA ...@@ -143,7 +143,7 @@ module QA
Page::Admin::Overview::Users::Show.perform do |show| Page::Admin::Overview::Users::Show.perform do |show|
user.id = show.user_id.to_i user.id = show.user_id.to_i
show.approve_user show.approve_user(user)
end end
expect(page).to have_text('Successfully approved') expect(page).to have_text('Successfully approved')
......
...@@ -4,6 +4,8 @@ require 'spec_helper' ...@@ -4,6 +4,8 @@ require 'spec_helper'
# Test an operation that triggers background jobs requiring administrative rights # Test an operation that triggers background jobs requiring administrative rights
RSpec.describe 'Admin mode for workers', :request_store do RSpec.describe 'Admin mode for workers', :request_store do
include Spec::Support::Helpers::Features::AdminUsersHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user_to_delete) { create(:user) } let(:user_to_delete) { create(:user) }
...@@ -37,7 +39,8 @@ RSpec.describe 'Admin mode for workers', :request_store do ...@@ -37,7 +39,8 @@ RSpec.describe 'Admin mode for workers', :request_store do
it 'can delete user', :js do it 'can delete user', :js do
visit admin_user_path(user_to_delete) 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 page.within '.modal-dialog' do
find("input[name='username']").send_keys(user_to_delete.name) find("input[name='username']").send_keys(user_to_delete.name)
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Admin::Users::User' do 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(:user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:current_user) { create(:admin) } let_it_be(:current_user) { create(:admin) }
...@@ -12,15 +14,18 @@ RSpec.describe 'Admin::Users::User' do ...@@ -12,15 +14,18 @@ RSpec.describe 'Admin::Users::User' do
end end
describe 'GET /admin/users/:id' do 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) visit admin_user_path(user)
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_content("ID: #{user.id}") expect(page).to have_content("ID: #{user.id}")
expect(page).to have_content("Namespace ID: #{user.namespace_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')
expect(page).to have_button('Delete user and contributions') expect(page).to have_button('Delete user and contributions')
end end
...@@ -29,9 +34,7 @@ RSpec.describe 'Admin::Users::User' do ...@@ -29,9 +34,7 @@ RSpec.describe 'Admin::Users::User' do
it 'shows confirmation and allows blocking and unblocking', :js do it 'shows confirmation and allows blocking and unblocking', :js do
visit admin_user_path(user) visit admin_user_path(user)
find('button', text: 'Block user').click click_action_in_user_dropdown(user.id, 'Block')
wait_for_requests
expect(page).to have_content('Block user') expect(page).to have_content('Block user')
expect(page).to have_content('You can always unblock their account, their data will remain intact.') 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 ...@@ -41,21 +44,18 @@ RSpec.describe 'Admin::Users::User' do
wait_for_requests wait_for_requests
expect(page).to have_content('Successfully blocked') expect(page).to have_content('Successfully blocked')
expect(page).to have_content('This user is blocked')
find('button', text: 'Unblock user').click click_action_in_user_dropdown(user.id, 'Unblock')
wait_for_requests
expect(page).to have_content('Unblock user') expect(page).to have_content('Unblock user')
expect(page).to have_content('You can always block their account again if needed.') expect(page).to have_content('You can always block their account again if needed.')
find('.modal-footer button', text: 'Unblock').click find('.modal-footer button', text: 'Unblock').click
wait_for_requests
expect(page).to have_content('Successfully unblocked') 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
end end
...@@ -63,9 +63,7 @@ RSpec.describe 'Admin::Users::User' do ...@@ -63,9 +63,7 @@ RSpec.describe 'Admin::Users::User' do
it 'shows confirmation and allows deactivating/re-activating', :js do it 'shows confirmation and allows deactivating/re-activating', :js do
visit admin_user_path(user) visit admin_user_path(user)
find('button', text: 'Deactivate user').click click_action_in_user_dropdown(user.id, 'Deactivate')
wait_for_requests
expect(page).to have_content('Deactivate user') expect(page).to have_content('Deactivate user')
expect(page).to have_content('You can always re-activate their account, their data will remain intact.') 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 ...@@ -75,11 +73,8 @@ RSpec.describe 'Admin::Users::User' do
wait_for_requests wait_for_requests
expect(page).to have_content('Successfully deactivated') 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('Activate user')
expect(page).to have_content('You can always deactivate their account again if needed.') expect(page).to have_content('You can always deactivate their account again if needed.')
...@@ -89,7 +84,9 @@ RSpec.describe 'Admin::Users::User' do ...@@ -89,7 +84,9 @@ RSpec.describe 'Admin::Users::User' do
wait_for_requests wait_for_requests
expect(page).to have_content('Successfully activated') 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
end end
...@@ -367,8 +364,11 @@ RSpec.describe 'Admin::Users::User' do ...@@ -367,8 +364,11 @@ RSpec.describe 'Admin::Users::User' do
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_content('Pending approval') 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 end
end end
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Admin::Users' do 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(:user, reload: true) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:current_user) { create(:admin) } let_it_be(:current_user) { create(:admin) }
...@@ -572,12 +574,6 @@ RSpec.describe 'Admin::Users' do ...@@ -572,12 +574,6 @@ RSpec.describe 'Admin::Users' do
end end
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 def first_row
page.all('[role="row"]')[1] page.all('[role="row"]')[1]
end end
...@@ -592,14 +588,4 @@ RSpec.describe 'Admin::Users' do ...@@ -592,14 +588,4 @@ RSpec.describe 'Admin::Users' do
click_link option click_link option
end end
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 end
import { GlDropdownDivider } from '@gitlab/ui'; import { GlDropdownDivider } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Actions from '~/admin/users/components/actions'; import Actions from '~/admin/users/components/actions';
import AdminUserActions from '~/admin/users/components/user_actions.vue'; import AdminUserActions from '~/admin/users/components/user_actions.vue';
...@@ -20,7 +21,7 @@ describe('AdminUserActions component', () => { ...@@ -20,7 +21,7 @@ describe('AdminUserActions component', () => {
findUserActions(id).find('[data-testid="dropdown-toggle"]'); findUserActions(id).find('[data-testid="dropdown-toggle"]');
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const initComponent = ({ actions = [] } = {}) => { const initComponent = ({ actions = [], showButtonLabels } = {}) => {
wrapper = shallowMountExtended(AdminUserActions, { wrapper = shallowMountExtended(AdminUserActions, {
propsData: { propsData: {
user: { user: {
...@@ -28,6 +29,10 @@ describe('AdminUserActions component', () => { ...@@ -28,6 +29,10 @@ describe('AdminUserActions component', () => {
actions, actions,
}, },
paths, paths,
showButtonLabels,
},
directives: {
GlTooltip: createMockDirective(),
}, },
}); });
}; };
...@@ -144,4 +149,42 @@ describe('AdminUserActions component', () => { ...@@ -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 { 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 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', () => { describe('initAdminUsersApp', () => {
let wrapper; let wrapper;
...@@ -14,15 +15,12 @@ describe('initAdminUsersApp', () => { ...@@ -14,15 +15,12 @@ describe('initAdminUsersApp', () => {
el.setAttribute('data-users', JSON.stringify(users)); el.setAttribute('data-users', JSON.stringify(users));
el.setAttribute('data-paths', JSON.stringify(paths)); el.setAttribute('data-paths', JSON.stringify(paths));
document.body.appendChild(el);
wrapper = createWrapper(initAdminUsersApp(el)); wrapper = createWrapper(initAdminUsersApp(el));
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
el.remove();
el = null; el = null;
}); });
...@@ -33,3 +31,31 @@ describe('initAdminUsersApp', () => { ...@@ -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 = [ ...@@ -18,6 +18,8 @@ export const users = [
}, },
]; ];
export const user = users[0];
export const paths = { export const paths = {
edit: '/admin/users/id/edit', edit: '/admin/users/id/edit',
approve: '/admin/users/id/approve', approve: '/admin/users/id/approve',
......
...@@ -396,4 +396,22 @@ RSpec.describe UsersHelper do ...@@ -396,4 +396,22 @@ RSpec.describe UsersHelper do
end end
end 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 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