diff options
Diffstat (limited to 'app/assets/javascripts/members/components/table')
7 files changed, 147 insertions, 40 deletions
diff --git a/app/assets/javascripts/members/components/table/created_at.vue b/app/assets/javascripts/members/components/table/created_at.vue index 0bad70894f9..44d124ad0db 100644 --- a/app/assets/javascripts/members/components/table/created_at.vue +++ b/app/assets/javascripts/members/components/table/created_at.vue @@ -1,10 +1,10 @@ <script> import { GlSprintf } from '@gitlab/ui'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import UserDate from '~/vue_shared/components/user_date.vue'; export default { name: 'CreatedAt', - components: { GlSprintf, TimeAgoTooltip }, + components: { GlSprintf, UserDate }, props: { date: { type: String, @@ -29,12 +29,12 @@ export default { <span> <gl-sprintf v-if="showCreatedBy" :message="s__('Members|%{time} by %{user}')"> <template #time> - <time-ago-tooltip :time="date" /> + <user-date :date="date" /> </template> <template #user> <a :href="createdBy.webUrl">{{ createdBy.name }}</a> </template> </gl-sprintf> - <time-ago-tooltip v-else :time="date" /> + <user-date v-else :date="date" /> </span> </template> diff --git a/app/assets/javascripts/members/components/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue index ecc2ed82ad0..6ec7be608ba 100644 --- a/app/assets/javascripts/members/components/table/member_action_buttons.vue +++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue @@ -3,12 +3,12 @@ import { MEMBER_TYPES, EE_ACTION_BUTTONS } from 'ee_else_ce/members/constants'; import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue'; import GroupActionButtons from '../action_buttons/group_action_buttons.vue'; import InviteActionButtons from '../action_buttons/invite_action_buttons.vue'; -import UserActionButtons from '../action_buttons/user_action_buttons.vue'; +import UserActionDropdown from '../action_dropdowns/user_action_dropdown.vue'; export default { name: 'MemberActionButtons', components: { - UserActionButtons, + UserActionDropdown, GroupActionButtons, InviteActionButtons, AccessRequestActionButtons, @@ -32,15 +32,11 @@ export default { type: Boolean, required: true, }, - isInvitedUser: { - type: Boolean, - required: true, - }, }, computed: { actionButtonComponent() { const dictionary = { - [MEMBER_TYPES.user]: 'user-action-buttons', + [MEMBER_TYPES.user]: 'user-action-dropdown', [MEMBER_TYPES.group]: 'group-action-buttons', [MEMBER_TYPES.invite]: 'invite-action-buttons', [MEMBER_TYPES.accessRequest]: 'access-request-action-buttons', @@ -60,6 +56,5 @@ export default { :member="member" :permissions="permissions" :is-current-user="isCurrentUser" - :is-invited-user="isInvitedUser" /> </template> diff --git a/app/assets/javascripts/members/components/table/member_activity.vue b/app/assets/javascripts/members/components/table/member_activity.vue new file mode 100644 index 00000000000..3b223cb1afa --- /dev/null +++ b/app/assets/javascripts/members/components/table/member_activity.vue @@ -0,0 +1,38 @@ +<script> +import UserDate from '~/vue_shared/components/user_date.vue'; + +export default { + components: { UserDate }, + props: { + member: { + type: Object, + required: true, + }, + }, + computed: { + userCreated() { + return this.member.user?.createdAt; + }, + lastActivity() { + return this.member.user?.lastActivityOn; + }, + }, +}; +</script> + +<template> + <div> + <div v-if="userCreated"> + <strong>{{ s__('Members|User created') }}:</strong> + <user-date :date="userCreated" /> + </div> + <div v-if="member.createdAt"> + <strong>{{ s__('Members|Access granted') }}:</strong> + <user-date :date="member.createdAt" /> + </div> + <div v-if="lastActivity"> + <strong>{{ s__('Members|Last activity') }}:</strong> + <user-date :date="lastActivity" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue index 30fcbfcd3f8..ed1971d020b 100644 --- a/app/assets/javascripts/members/components/table/member_source.vue +++ b/app/assets/javascripts/members/components/table/member_source.vue @@ -1,11 +1,19 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; export default { name: 'MemberSource', + i18n: { + inherited: __('Inherited'), + directMember: __('Direct member'), + directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'), + inheritedMemberWithCreatedBy: s__('Members|%{group} by %{createdBy}'), + }, directives: { GlTooltip: GlTooltipDirective, }, + components: { GlSprintf }, props: { memberSource: { type: Object, @@ -15,13 +23,40 @@ export default { type: Boolean, required: true, }, + createdBy: { + type: Object, + required: false, + default: null, + }, + }, + computed: { + showCreatedBy() { + return this.createdBy?.name && this.createdBy?.webUrl; + }, + messageWithCreatedBy() { + return this.isDirectMember + ? this.$options.i18n.directMemberWithCreatedBy + : this.$options.i18n.inheritedMemberWithCreatedBy; + }, }, }; </script> <template> - <span v-if="isDirectMember">{{ __('Direct member') }}</span> - <a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{ + <span v-if="showCreatedBy"> + <gl-sprintf :message="messageWithCreatedBy"> + <template #group> + <a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{ + memberSource.fullName + }}</a> + </template> + <template #createdBy> + <a :href="createdBy.webUrl">{{ createdBy.name }}</a> + </template> + </gl-sprintf> + </span> + <span v-else-if="isDirectMember">{{ $options.i18n.directMember }}</span> + <a v-else v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{ memberSource.fullName }}</a> </template> diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index 0512bc04085..8f03a298e63 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -2,14 +2,20 @@ import { GlTable, GlBadge, GlPagination } from '@gitlab/ui'; import { mapState } from 'vuex'; import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue'; -import { canUnban, canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils'; +import { + canDisableTwoFactor, + canUnban, + canOverride, + canRemove, + canRemoveBlockedByLastOwner, + canResend, + canUpdate, +} from 'ee_else_ce/members/utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import UserDate from '~/vue_shared/components/user_date.vue'; import { FIELD_KEY_ACTIONS, FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME, - TAB_QUERY_PARAM_VALUES, MEMBER_STATE_AWAITING, MEMBER_STATE_ACTIVE, USER_STATE_BLOCKED, @@ -23,6 +29,7 @@ import ExpirationDatepicker from './expiration_datepicker.vue'; import MemberActionButtons from './member_action_buttons.vue'; import MemberAvatar from './member_avatar.vue'; import MemberSource from './member_source.vue'; +import MemberActivity from './member_activity.vue'; import RoleDropdown from './role_dropdown.vue'; export default { @@ -40,11 +47,13 @@ export default { RemoveGroupLinkModal, RemoveMemberModal, ExpirationDatepicker, - UserDate, + MemberActivity, + DisableTwoFactorModal: () => + import('ee_component/members/components/modals/disable_two_factor_modal.vue'), LdapOverrideConfirmationModal: () => import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'), }, - inject: ['namespace', 'currentUserId'], + inject: ['namespace', 'currentUserId', 'canManageMembers'], props: { tabQueryParamValue: { type: String, @@ -80,18 +89,17 @@ export default { return paramName && currentPage && perPage && totalItems; }, - isInvitedUser() { - return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite; - }, }, methods: { hasActionButtons(member) { return ( canRemove(member) || + canRemoveBlockedByLastOwner(member, this.canManageMembers) || canResend(member) || canUpdate(member, this.currentUserId) || canOverride(member) || - canUnban(member) + canUnban(member) || + canDisableTwoFactor(member) ); }, showField(field) { @@ -249,7 +257,11 @@ export default { <template #cell(source)="{ item: member }"> <members-table-cell #default="{ isDirectMember }" :member="member"> - <member-source :is-direct-member="isDirectMember" :member-source="member.source" /> + <member-source + :is-direct-member="isDirectMember" + :member-source="member.source" + :created-by="member.createdBy" + /> </members-table-cell> </template> @@ -281,12 +293,8 @@ export default { </members-table-cell> </template> - <template #cell(userCreatedAt)="{ item: member }"> - <user-date :date="member.user.createdAt" /> - </template> - - <template #cell(lastActivityOn)="{ item: member }"> - <user-date :date="member.user.lastActivityOn" /> + <template #cell(activity)="{ item: member }"> + <member-activity :member="member" /> </template> <template #cell(actions)="{ item: member }"> @@ -294,7 +302,6 @@ export default { <member-action-buttons :member-type="memberType" :is-current-user="isCurrentUser" - :is-invited-user="isInvitedUser" :permissions="permissions" :member="member" /> @@ -317,6 +324,7 @@ export default { :label-prev-page="__('Go to previous page')" align="center" /> + <disable-two-factor-modal /> <remove-group-link-modal /> <remove-member-modal /> <ldap-override-confirmation-modal /> diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue index 51eff428d63..407cbc55dd3 100644 --- a/app/assets/javascripts/members/components/table/members_table_cell.vue +++ b/app/assets/javascripts/members/components/table/members_table_cell.vue @@ -5,13 +5,14 @@ import { isDirectMember, isCurrentUser, canRemove, + canRemoveBlockedByLastOwner, canResend, canUpdate, } from '../../utils'; export default { name: 'MembersTableCell', - inject: ['currentUserId'], + inject: ['currentUserId', 'canManageMembers'], props: { member: { type: Object, @@ -45,6 +46,9 @@ export default { isCurrentUser() { return isCurrentUser(this.member, this.currentUserId); }, + canRemoveBlockedByLastOwner() { + return canRemoveBlockedByLastOwner(this.member, this.canManageMembers); + }, canRemove() { return canRemove(this.member); }, @@ -62,6 +66,7 @@ export default { isCurrentUser: this.isCurrentUser, permissions: { canRemove: this.canRemove, + canRemoveBlockedByLastOwner: this.canRemoveBlockedByLastOwner, canResend: this.canResend, canUpdate: this.canUpdate, }, diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue index 6cd8bf57313..70808587d56 100644 --- a/app/assets/javascripts/members/components/table/role_dropdown.vue +++ b/app/assets/javascripts/members/components/table/role_dropdown.vue @@ -2,7 +2,9 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { mapActions } from 'vuex'; +import * as Sentry from '@sentry/browser'; import { s__ } from '~/locale'; +import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action'; export default { name: 'RoleDropdown', @@ -11,7 +13,7 @@ export default { GlDropdownItem, LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'), }, - inject: ['namespace'], + inject: ['namespace', 'group'], props: { member: { type: Object, @@ -30,7 +32,7 @@ export default { }, computed: { disabled() { - return this.busy || (this.permissions.canOverride && !this.member.isOverridden); + return this.permissions.canOverride && !this.member.isOverridden; }, }, mounted() { @@ -50,22 +52,45 @@ export default { return dispatch(`${this.namespace}/updateMemberRole`, payload); }, }), - handleSelect(value, name) { - if (value === this.member.accessLevel.integerValue) { + async handleOverageConfirm(currentRoleValue, newRoleValue, newRoleName) { + return guestOverageConfirmAction({ + currentRoleValue, + newRoleValue, + newRoleName, + group: this.group, + memberId: this.member.id, + memberType: this.namespace, + }); + }, + async handleSelect(newRoleValue, newRoleName) { + const currentRoleValue = this.member.accessLevel.integerValue; + if (newRoleValue === currentRoleValue) { return; } this.busy = true; + const confirmed = await this.handleOverageConfirm( + currentRoleValue, + newRoleValue, + newRoleName, + ); + if (!confirmed) { + this.busy = false; + return; + } + this.updateMemberRole({ memberId: this.member.id, - accessLevel: { integerValue: value, stringValue: name }, + accessLevel: { integerValue: newRoleValue, stringValue: newRoleName }, }) .then(() => { this.$toast.show(s__('Members|Role updated successfully.')); - this.busy = false; }) - .catch(() => { + .catch((error) => { + Sentry.captureException(error); + }) + .finally(() => { this.busy = false; }); }, @@ -80,6 +105,7 @@ export default { :text="member.accessLevel.stringValue" :header-text="__('Change role')" :disabled="disabled" + :loading="busy" > <gl-dropdown-item v-for="(value, name) in member.validRoles" |