diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-19 21:08:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-19 21:08:23 +0300 |
commit | 3f0db3db2ad99a74c3969bf2e930814004ccf1ec (patch) | |
tree | c8b4123b3b4b422b14211430b85556e64eb68a26 /app | |
parent | 4a721269429a178957e8ce7c6d0a75d3307c9830 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
28 files changed, 178 insertions, 378 deletions
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue index b782526e6be..c076e0bedf0 100644 --- a/app/assets/javascripts/admin/users/components/user_actions.vue +++ b/app/assets/javascripts/admin/users/components/user_actions.vue @@ -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> diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js index 33ee7e1cb0d..4636c8705a5 100644 --- a/app/assets/javascripts/admin/users/constants.js +++ b/app/assets/javascripts/admin/users/constants.js @@ -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'), diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js index 05f8469e61a..852b253d25a 100644 --- a/app/assets/javascripts/admin/users/index.js +++ b/app/assets/javascripts/admin/users/index.js @@ -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); diff --git a/app/assets/javascripts/pages/admin/identities/index.js b/app/assets/javascripts/pages/admin/identities/index.js index 868c8e33077..a9f5f00cb9b 100644 --- a/app/assets/javascripts/pages/admin/identities/index.js +++ b/app/assets/javascripts/pages/admin/identities/index.js @@ -1,3 +1,6 @@ +import { initAdminUserActions, initDeleteUserModals } from '~/admin/users'; import initConfirmModal from '~/confirm_modal'; +initAdminUserActions(); +initDeleteUserModals(); initConfirmModal(); diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js index 3f9e4e9d591..8fbc8dc17bc 100644 --- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -1,5 +1,8 @@ import { initExpiresAtField } from '~/access_tokens'; +import { initAdminUserActions, initDeleteUserModals } from '~/admin/users'; import initConfirmModal from '~/confirm_modal'; +initAdminUserActions(); +initDeleteUserModals(); initExpiresAtField(); initConfirmModal(); diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 99bf842ef2d..41e99a3baf5 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -1,6 +1,7 @@ -import { initAdminUsersApp, initDeleteUserModals } from '~/admin/users'; +import { initAdminUsersApp, initDeleteUserModals, initAdminUserActions } from '~/admin/users'; import initConfirmModal from '~/confirm_modal'; initAdminUsersApp(); +initAdminUserActions(); initDeleteUserModals(); initConfirmModal(); diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js index f1d9ced807b..b36c9c0d049 100644 --- a/app/assets/javascripts/pipelines/components/parsing_utils.js +++ b/app/assets/javascripts/pipelines/components/parsing_utils.js @@ -1,4 +1,4 @@ -import { isEqual, memoize, uniqWith } from 'lodash'; +import { memoize } from 'lodash'; import { createSankey } from './dag/drawing_utils'; /* @@ -113,11 +113,24 @@ export const filterByAncestors = (links, nodeDict) => return !allAncestors.includes(source); }); +/* + A peformant alternative to lodash's isEqual. Because findIndex always finds + the first instance of a match, if the found index is not the first, we know + it is in fact a duplicate. +*/ +const deduplicate = (item, itemIndex, arr) => { + const foundIdx = arr.findIndex((test) => { + return test.source === item.source && test.target === item.target; + }); + + return foundIdx === itemIndex; +}; + export const parseData = (nodes) => { const nodeDict = createNodeDict(nodes); const allLinks = makeLinksFromNodes(nodes, nodeDict); const filteredLinks = filterByAncestors(allLinks, nodeDict); - const links = uniqWith(filteredLinks, isEqual); + const links = filteredLinks.filter(deduplicate); return { nodes, links }; }; diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 1ff2ad01b63..ea101cf1dcd 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -83,7 +83,10 @@ class MembersFinder union = Gitlab::SQL::Union.new(union_members, remove_duplicates: false) # rubocop: disable Gitlab/Union sql = distinct_on(union) - Member.includes(:user).from([Arel.sql("(#{sql}) AS #{Member.table_name}")]) # rubocop: disable CodeReuse/ActiveRecord + # enumerate the columns here since we are enumerating them in the union and want to be immune to + # column caching issues when adding/removing columns + Member.select(*Member.column_names) + .includes(:user).from([Arel.sql("(#{sql}) AS #{Member.table_name}")]) # rubocop: disable CodeReuse/ActiveRecord end def distinct_on(union) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 2af932f8e8c..93a0166f43e 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -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 diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index ab044b80133..cf5906a4cbf 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -28,6 +28,8 @@ class GroupMember < Member attr_accessor :last_owner, :last_blocked_owner + self.enumerate_columns_in_select_statements = true + def self.access_level_roles Gitlab::Access.options_with_owner end diff --git a/app/services/groups/group_links/create_service.rb b/app/services/groups/group_links/create_service.rb index 5f81e5972b0..8c3ba0a63f2 100644 --- a/app/services/groups/group_links/create_service.rb +++ b/app/services/groups/group_links/create_service.rb @@ -24,7 +24,7 @@ module Groups ) if link.save - shared_with_group.refresh_members_authorized_projects(direct_members_only: true) + shared_with_group.refresh_members_authorized_projects(blocking: false, direct_members_only: true) success(link: link) else error(link.errors.full_messages.to_sentence, 409) diff --git a/app/services/groups/group_links/destroy_service.rb b/app/services/groups/group_links/destroy_service.rb index 05504a80f46..0e7fd7e0817 100644 --- a/app/services/groups/group_links/destroy_service.rb +++ b/app/services/groups/group_links/destroy_service.rb @@ -16,7 +16,7 @@ module Groups groups_to_refresh = links.map(&:shared_with_group) groups_to_refresh.uniq.each do |group| - group.refresh_members_authorized_projects(direct_members_only: true) + group.refresh_members_authorized_projects(blocking: false, direct_members_only: true) end else Gitlab::AppLogger.info( diff --git a/app/services/groups/group_links/update_service.rb b/app/services/groups/group_links/update_service.rb index 3703d535482..a1411de36d6 100644 --- a/app/services/groups/group_links/update_service.rb +++ b/app/services/groups/group_links/update_service.rb @@ -13,7 +13,7 @@ module Groups group_link.update!(group_link_params) if requires_authorization_refresh?(group_link_params) - group_link.shared_with_group.refresh_members_authorized_projects(direct_members_only: true) + group_link.shared_with_group.refresh_members_authorized_projects(blocking: false, direct_members_only: true) end end diff --git a/app/services/jira_import/users_mapper_service.rb b/app/services/jira_import/users_mapper_service.rb index 760f06a1cfb..13e0dd5120e 100644 --- a/app/services/jira_import/users_mapper_service.rb +++ b/app/services/jira_import/users_mapper_service.rb @@ -77,7 +77,7 @@ module JiraImport end def project_member_ids - @project_member_ids ||= MembersFinder.new(project, current_user).execute.select(:user_id) + @project_member_ids ||= MembersFinder.new(project, current_user).execute.reselect(:user_id) end end end diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index a6d562dad31..d85ab476693 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -15,3 +15,5 @@ = render @identities - else %h4= _('This user has no identities') + += render partial: 'admin/users/modals' diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml index 465025ba684..1609687fc8d 100644 --- a/app/views/admin/impersonation_tokens/index.html.haml +++ b/app/views/admin/impersonation_tokens/index.html.haml @@ -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' diff --git a/app/views/admin/users/_approve_user.html.haml b/app/views/admin/users/_approve_user.html.haml deleted file mode 100644 index f61c9fa4b80..00000000000 --- a/app/views/admin/users/_approve_user.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.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' } diff --git a/app/views/admin/users/_ban_user.html.haml b/app/views/admin/users/_ban_user.html.haml deleted file mode 100644 index 229c88adb7f..00000000000 --- a/app/views/admin/users/_ban_user.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- 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') diff --git a/app/views/admin/users/_block_user.html.haml b/app/views/admin/users/_block_user.html.haml deleted file mode 100644 index 29029986345..00000000000 --- a/app/views/admin/users/_block_user.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.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') diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 9ef2d7b2f22..b7b712e078d 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -1,38 +1,38 @@ -%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) diff --git a/app/views/admin/users/_reject_pending_user.html.haml b/app/views/admin/users/_reject_pending_user.html.haml deleted file mode 100644 index 17108427330..00000000000 --- a/app/views/admin/users/_reject_pending_user.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.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?') } diff --git a/app/views/admin/users/_user_activation_effects.html.haml b/app/views/admin/users/_user_activation_effects.html.haml deleted file mode 100644 index 244836dac11..00000000000 --- a/app/views/admin/users/_user_activation_effects.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%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' diff --git a/app/views/admin/users/_user_approve_effects.html.haml b/app/views/admin/users/_user_approve_effects.html.haml deleted file mode 100644 index 54e51bf3467..00000000000 --- a/app/views/admin/users/_user_approve_effects.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%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') diff --git a/app/views/admin/users/_user_detail_note.html.haml b/app/views/admin/users/_user_detail_note.html.haml index 4f2a682c5ca..cc4827327c9 100644 --- a/app/views/admin/users/_user_detail_note.html.haml +++ b/app/views/admin/users/_user_detail_note.html.haml @@ -1,7 +1,7 @@ - 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 diff --git a/app/views/admin/users/_user_reject_effects.html.haml b/app/views/admin/users/_user_reject_effects.html.haml deleted file mode 100644 index 17b6862b0cc..00000000000 --- a/app/views/admin/users/_user_reject_effects.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%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 } diff --git a/app/views/admin/users/keys.html.haml b/app/views/admin/users/keys.html.haml index 5f9d11af7c1..28024ae084f 100644 --- a/app/views/admin/users/keys.html.haml +++ b/app/views/admin/users/keys.html.haml @@ -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' diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 3ff726e1945..8c56e888dcc 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -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' diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 5477ece7439..ad8d9d1f04f 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -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' |