Create guest overage confirmation component
What does this MR do and why?
GitLab subscriptions come with a certain number of user seats. For user roles, some roles will take up a seat, and some roles don't (for example a Guest role).
On the Manage
-> Members
page, a list of members is shown for the project/group. In the Max Role column, a user's role can be changed. When the role is changed from one that doesn't take up a seat to one that does, we check to see if this will cause a seat usage overage, and show a warning modal if it will:
Currently, the warning modal is constructed purely using JS in these 2 files:
app/assets/javascripts/members/guest_overage_confirm_action.js
ee/app/assets/javascripts/members/guest_overage_confirm_action.js
This is awkward to maintain because rather than using a Vue component, it creates one using pure Javascript. This MR refactors it into an actual Vue component, and also fixes several bugs (see comments for details). Note that this component is currently unused, this is some prerequisite work for a follow-up MR. Also note that the feature itself is also unused, the feature flag exists but defaults to false and is not enabled on production.
How to set up and validate locally
The component is currently unused and will be used in a future MR, so we will modify the current role dropdown to use it. Apply this patch first:
Patch
Index: ee/app/assets/javascripts/members/components/table/guest_overage_confirmation.vue
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/ee/app/assets/javascripts/members/components/table/guest_overage_confirmation.vue b/ee/app/assets/javascripts/members/components/table/guest_overage_confirmation.vue
--- a/ee/app/assets/javascripts/members/components/table/guest_overage_confirmation.vue (revision 2147f5bdd20f93c8bef58a3e4df056c692dd5bea)
+++ b/ee/app/assets/javascripts/members/components/table/guest_overage_confirmation.vue (date 1716628685698)
@@ -73,36 +73,42 @@
// Check to see if changing the role would increase the seat usage and cause an overage, and if so, show a warning
// modal. Otherwise, act as if the overage warning was accepted and emit the confirm event.
async confirmOverage() {
- if (this.shouldSkipConfirmationCheck) {
- return this.emitConfirm();
- }
+ try {
+ if (this.shouldSkipConfirmationCheck) {
+ this.emitConfirm();
+ return;
+ }
- const response = await this.$apollo.query({
- query: getBillableUserCountChanges,
- fetchPolicy: fetchPolicies.NO_CACHE,
- variables: {
- fullPath: this.groupPath,
- addGroupId: this.isGroup ? this.member.id : null,
- addUserIds: this.isGroup ? null : [this.member.id],
- addUserEmails: [],
- role: ACCESS_LEVEL_LABELS[this.role.accessLevel].toUpperCase(),
- memberRoleId: this.role.memberRoleId,
- },
- });
+ const response = await this.$apollo.query({
+ query: getBillableUserCountChanges,
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ variables: {
+ fullPath: this.groupPath,
+ addGroupId: this.isGroup ? this.member.id : null,
+ addUserIds: this.isGroup ? null : [this.member.id],
+ addUserEmails: [],
+ role: ACCESS_LEVEL_LABELS[this.role.accessLevel].toUpperCase(),
+ memberRoleId: this.role.memberRoleId,
+ },
+ });
- const { willIncreaseOverage, seatsInSubscription, newBillableUserCount } =
- response?.data?.group?.gitlabSubscriptionsPreviewBillableUserChange || {};
- // If the overage won't increase or if there's no subscription data, don't show the modal.
- if (!willIncreaseOverage || isNil(seatsInSubscription) || isNil(newBillableUserCount)) {
- return this.emitConfirm();
- }
+ const { willIncreaseOverage, seatsInSubscription, newBillableUserCount } =
+ response?.data?.group?.gitlabSubscriptionsPreviewBillableUserChange || {};
+ // If the overage won't increase or if there's no subscription data, don't show the modal.
+ if (!willIncreaseOverage || isNil(seatsInSubscription) || isNil(newBillableUserCount)) {
+ // this.emitConfirm();
+ // return;
+ }
- // Overage check is valid, set a bunch of values and show the modal.
- this.groupName = response.data.group.name;
- this.seatsInSubscription = seatsInSubscription;
- this.newBillableUserCount = newBillableUserCount;
+ // Overage check is valid, set a bunch of values and show the modal.
+ this.groupName = response.data.group.name;
+ this.seatsInSubscription = seatsInSubscription;
+ this.newBillableUserCount = newBillableUserCount;
- return this.$refs.modal.show();
+ this.$refs.modal.show();
+ } catch (error) {
+ this.$emit('error', error);
+ }
},
emitConfirm() {
this.$emit('confirm');
Index: app/assets/javascripts/members/components/table/max_role.vue
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/app/assets/javascripts/members/components/table/max_role.vue b/app/assets/javascripts/members/components/table/max_role.vue
--- a/app/assets/javascripts/members/components/table/max_role.vue (revision 2147f5bdd20f93c8bef58a3e4df056c692dd5bea)
+++ b/app/assets/javascripts/members/components/table/max_role.vue (date 1716628646514)
@@ -10,9 +10,10 @@
initialSelectedRole,
handleMemberRoleUpdate,
} from 'ee_else_ce/members/utils';
-import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_GUEST_INTEGER } from '~/access_level/constants';
import { s__ } from '~/locale';
import { logError } from '~/lib/logger';
+import { createAlert } from '~/alert';
export default {
components: {
@@ -22,6 +23,8 @@
import('ee_component/members/components/action_dropdowns/ldap_dropdown_footer.vue'),
ManageRolesDropdownFooter: () =>
import('ee_component/members/components/action_dropdowns/manage_roles_dropdown_footer.vue'),
+ GuestOverageConfirmation: () =>
+ import('ee_else_ce/members/components/table/guest_overage_confirmation.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -46,12 +49,23 @@
isDesktop: false,
memberRoleId: this.member.accessLevel.memberRoleId ?? null,
selectedRole: initialSelectedRole(accessLevelOptions.flatten, this.member),
+ previousRole: null,
+ previousMemberRoleId: null,
};
},
computed: {
disabled() {
return this.permissions.canOverride && !this.member.isOverridden;
},
+ currentRole() {
+ const role = this.accessLevelOptions.flatten.find((item) => item.value === this.selectedRole);
+
+ if (!role.memberRoleId && !role.occupiesSeat) {
+ role.occupiesSeat = role.accessLevel > ACCESS_LEVEL_GUEST_INTEGER;
+ }
+
+ return role;
+ },
},
mounted() {
this.isDesktop = bp.isDesktop();
@@ -68,49 +82,46 @@
}),
async handleSelect(value) {
this.busy = true;
-
- const newRole = this.accessLevelOptions.flatten.find((item) => item.value === value);
- const previousRole = this.selectedRole;
- const previousMemberRoleId = this.memberRoleId;
-
- try {
- const confirmed = await guestOverageConfirmAction({
- oldAccessLevel: this.member.accessLevel.integerValue,
- newRoleName: ACCESS_LEVEL_LABELS[newRole.accessLevel],
- newMemberRoleId: newRole.memberRoleId,
- group: this.group,
- memberId: this.member.id,
- memberType: this.namespace,
- });
- if (!confirmed) {
- return;
- }
-
- this.selectedRole = value;
- this.memberRoleId = newRole.memberRoleId;
-
+ this.previousRole = this.selectedRole;
+ this.previousMemberRoleId = this.currentRole.memberRoleId;
+ this.selectedRole = value;
+ await this.$nextTick();
+ this.$refs.overage.confirmOverage();
+ },
+ async updateRole() {
+ try {
const response = await this.updateMemberRole({
memberId: this.member.id,
- accessLevel: newRole.accessLevel,
- memberRoleId: newRole.memberRoleId,
+ accessLevel: this.currentRole.accessLevel,
+ memberRoleId: this.currentRole.memberRoleId,
});
// EE has a flow where role change is not immediate but goes through an approval process.
// In that case we should restore previously selected user role
this.selectedRole = handleMemberRoleUpdate({
- currentRole: previousRole,
- requestedRole: newRole.value,
+ currentRole: this.previousRole,
+ requestedRole: this.selectedRole,
response,
});
+
+ this.member.usingLicense = this.currentRole.occupiesSeat;
} catch (error) {
- this.selectedRole = previousRole;
- this.memberRoleId = previousMemberRoleId;
+ this.cancelUpdate();
logError(error);
Sentry.captureException(error);
} finally {
this.busy = false;
}
},
+ cancelUpdate() {
+ this.busy = false;
+ this.selectedRole = this.previousRole;
+ this.previousMemberRoleId = this.memberRoleId;
+ },
+ handleError({ message }) {
+ createAlert({ message });
+ this.cancelUpdate();
+ },
},
i18n: {
customRole: s__('MemberRole|Custom role'),
@@ -120,6 +131,15 @@
<template>
<div>
+ <guest-overage-confirmation
+ ref="overage"
+ :group-path="group.path"
+ :member="member"
+ :role="currentRole"
+ @confirm="updateRole"
+ @cancel="cancelUpdate"
+ @error="handleError"
+ />
<gl-collapsible-listbox
v-if="permissions.canUpdate"
:placement="isDesktop ? 'left' : 'right'"
Index: ee/app/assets/javascripts/members/utils.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/ee/app/assets/javascripts/members/utils.js b/ee/app/assets/javascripts/members/utils.js
--- a/ee/app/assets/javascripts/members/utils.js (revision 2147f5bdd20f93c8bef58a3e4df056c692dd5bea)
+++ b/ee/app/assets/javascripts/members/utils.js (date 1716623394666)
@@ -62,12 +62,13 @@
const { flatten: staticRoleDropdownItems } = CERoleDropdownItems({ validRoles });
const customRoleDropdownItems = customRoles.map(
- ({ baseAccessLevel, name, memberRoleId, description }) => ({
+ ({ baseAccessLevel, name, memberRoleId, description, occupiesSeat }) => ({
accessLevel: baseAccessLevel,
memberRoleId,
text: name,
value: uniqueId('role-custom-'),
description,
+ occupiesSeat,
}),
);
Then follow this video walkthrough (with audio commentary):
These steps below are for reference:
- Enable the
:show_overage_on_role_promotion
feature flag:
echo "Feature.enable(:show_overage_on_role_promotion)" | rails c
- Enable SAAS mode, then restart GDK:
export GITLAB_SIMULATE_SAAS=1
gdk restart
-
Go to
Admin Area
->Settings
->General
-> expandAccount and limits
-> enableAllow use of licensed EE features
->Save changes
. -
Go to
Admin Area
->Overview
->Groups
. Click onEdit
for a group of your choice in the list. On the edit page, change thePlan
toUltimate
, then click onSave changes
at the bottom. -
Go to the group's
Settings
->Roles and Permissions
page. Click onNew role
and use it to create 2 new roles: one with a base role ofGuest
with onlyRead code
permission (doesn't take up a seat), and another role with a base role of your choice and any other permission (takes up a seat). -
Go to the group's
Manage
->Members
page. You should see that several users have theIs using seat
badge. -
For one of the users that has the
Is using seat
badge, change their role toGuest
. Verify that you do not get the warning modal. -
Change the role now to a standard role higher than
Guest
. Verify that you do get the warning modal. -
Click
Cancel
in the modal. Verify that the role is stillGuest
and not changed. Try it again, but this time clickContinue with overages
. Verify that the role is changed, and theIs using seat
badge is shown again. -
Repeat the above steps, but with the
Guest
custom role and the other custom role that you created. -
Open
ee/app/assets/javascripts/members/components/table/guest_overage_confirmation.vue
and throw an error just inside the try block:
async confirmCoverage() {
try {
throw new Error('some error');
...
- Try changing the user's role and verify that an error is shown.
Related to #456282 (closed)