From bcc2a5de1434dc14af0666c8d16e277544c4ab7d Mon Sep 17 00:00:00 2001
From: Vijay Hawoldar <vhawoldar@gitlab.com>
Date: Thu, 20 Feb 2025 08:21:17 +0000
Subject: [PATCH] Disable remove user button for last owners

Removing the last owner of a group from the seat usage page is not
possible, this disables the button with an appropriate tooltip

Changelog: changed
---
 .../components/subscription_user_list.vue     | 33 ++++++++++++-------
 .../subscription_user_list_spec.js.snap       | 12 +++++++
 .../components/subscription_user_list_spec.js | 33 +++++++++++--------
 locale/gitlab.pot                             |  3 ++
 4 files changed, 56 insertions(+), 25 deletions(-)

diff --git a/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_user_list.vue b/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_user_list.vue
index 8804336f26573..1b845696eaa82 100644
--- a/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_user_list.vue
+++ b/ee/app/assets/javascripts/usage_quotas/seats/components/subscription_user_list.vue
@@ -163,6 +163,9 @@ export default {
       if (this.removedBillableMemberId === user?.id) return true;
       return this.recentlyDeletedMembersIds.includes(user?.id);
     },
+    isLastOwner(user) {
+      return user.is_last_owner;
+    },
     getRecentlyDeletedMembersIds() {
       try {
         if (this.hasLocalStorageExpired()) {
@@ -174,6 +177,9 @@ export default {
         return [];
       }
     },
+    removeButtonDisabled(user) {
+      return this.isUserRemoved(user) || this.isLastOwner(user);
+    },
   },
   i18n: {
     emailNotVisibleTooltipText,
@@ -275,35 +281,40 @@ export default {
         </div>
       </template>
 
-      <template #cell(lastActivityTime)="data">
+      <template #cell(lastActivityTime)="{ item }">
         <span data-testid="last_activity_on">
-          {{ data.item.user.last_activity_on ? data.item.user.last_activity_on : __('Never') }}
+          {{ item.user.last_activity_on ? item.user.last_activity_on : __('Never') }}
         </span>
       </template>
 
-      <template #cell(lastLoginAt)="data">
+      <template #cell(lastLoginAt)="{ item }">
         <span data-testid="last_login_at">
-          {{ formatLastLoginAt(data.item.user.last_login_at) }}
+          {{ formatLastLoginAt(item.user.last_login_at) }}
         </span>
       </template>
 
-      <template #cell(actions)="data">
-        <span :id="`remove-member-${data.item.user.id}`" class="gl-inline-block" tabindex="0">
+      <template #cell(actions)="{ item }">
+        <span :id="`remove-member-${item.user.id}`" class="gl-inline-block" tabindex="0">
           <gl-button
             v-gl-modal="$options.removeBillableMemberModalId"
             category="secondary"
             variant="danger"
             data-testid="remove-user"
-            :disabled="isUserRemoved(data.item.user)"
-            @click="displayRemoveMemberModal(data.item.user)"
+            :disabled="removeButtonDisabled(item.user)"
+            @click="displayRemoveMemberModal(item.user)"
           >
             {{ __('Remove user') }}
           </gl-button>
           <gl-tooltip
-            v-if="isUserRemoved(data.item.user)"
-            :target="`remove-member-${data.item.user.id}`"
+            v-if="removeButtonDisabled(item.user)"
+            :target="`remove-member-${item.user.id}`"
+            data-testid="remove-user-tooltip"
           >
-            {{ s__('Billing|This user is scheduled for removal.') }}</gl-tooltip
+            {{
+              isLastOwner(item.user)
+                ? s__('Billing|Cannot remove the last owner.')
+                : s__('Billing|This user is scheduled for removal.')
+            }}</gl-tooltip
           >
         </span>
       </template>
diff --git a/ee/spec/frontend/usage_quotas/seats/components/__snapshots__/subscription_user_list_spec.js.snap b/ee/spec/frontend/usage_quotas/seats/components/__snapshots__/subscription_user_list_spec.js.snap
index 4f0c37ba23639..7d9f9d6fcd0d4 100644
--- a/ee/spec/frontend/usage_quotas/seats/components/__snapshots__/subscription_user_list_spec.js.snap
+++ b/ee/spec/frontend/usage_quotas/seats/components/__snapshots__/subscription_user_list_spec.js.snap
@@ -6,42 +6,54 @@ exports[`SubscriptionUserList renders table content renders the correct data 1`]
     "email": "administrator@email.com",
     "lastActivityOn": "2020-03-01",
     "lastLoginAt": "2022-11-10 10:58:05",
+    "removeUserButtonDisabled": true,
     "removeUserButtonExists": true,
+    "removeUserButtonTooltip": "Cannot remove the last owner.",
     "tooltip": undefined,
   },
   {
     "email": "agustin_walker@email.com",
     "lastActivityOn": "2020-03-01",
     "lastLoginAt": "2021-01-20 10:58:05",
+    "removeUserButtonDisabled": false,
     "removeUserButtonExists": true,
+    "removeUserButtonTooltip": undefined,
     "tooltip": undefined,
   },
   {
     "email": "Private",
     "lastActivityOn": "Never",
     "lastLoginAt": "Never",
+    "removeUserButtonDisabled": false,
     "removeUserButtonExists": true,
+    "removeUserButtonTooltip": undefined,
     "tooltip": "An email address is only visible for users with public emails.",
   },
   {
     "email": "jdoe@email.com",
     "lastActivityOn": "Never",
     "lastLoginAt": "Never",
+    "removeUserButtonDisabled": false,
     "removeUserButtonExists": true,
+    "removeUserButtonTooltip": undefined,
     "tooltip": undefined,
   },
   {
     "email": "jsnow@email.com",
     "lastActivityOn": "2020-03-01",
     "lastLoginAt": "Never",
+    "removeUserButtonDisabled": false,
     "removeUserButtonExists": true,
+    "removeUserButtonTooltip": undefined,
     "tooltip": undefined,
   },
   {
     "email": "current_user@email.com",
     "lastActivityOn": "2020-03-01",
     "lastLoginAt": "Never",
+    "removeUserButtonDisabled": false,
     "removeUserButtonExists": true,
+    "removeUserButtonTooltip": undefined,
     "tooltip": undefined,
   },
 ]
diff --git a/ee/spec/frontend/usage_quotas/seats/components/subscription_user_list_spec.js b/ee/spec/frontend/usage_quotas/seats/components/subscription_user_list_spec.js
index b84528c0499b1..5cd91efa9aa57 100644
--- a/ee/spec/frontend/usage_quotas/seats/components/subscription_user_list_spec.js
+++ b/ee/spec/frontend/usage_quotas/seats/components/subscription_user_list_spec.js
@@ -1,6 +1,5 @@
 import {
   GlPagination,
-  GlButton,
   GlTable,
   GlAvatarLink,
   GlAvatarLabeled,
@@ -120,14 +119,20 @@ describe('SubscriptionUserList', () => {
   const findErrorModal = () => wrapper.findComponent(GlModal);
 
   const serializeTableRow = (rowWrapper) => {
-    const emailWrapper = rowWrapper.find('[data-testid="email"]');
+    const extendedRowWrapper = extendedWrapper(rowWrapper);
+    const emailWrapper = extendedRowWrapper.findByTestId('email');
 
     return {
       email: emailWrapper.text(),
       tooltip: emailWrapper.find('span').attributes('title'),
-      removeUserButtonExists: rowWrapper.findComponent(GlButton).exists(),
-      lastActivityOn: rowWrapper.find('[data-testid="last_activity_on"]').text(),
-      lastLoginAt: rowWrapper.find('[data-testid="last_login_at"]').text(),
+      removeUserButtonExists: extendedRowWrapper.findByTestId('remove-user').exists(),
+      removeUserButtonDisabled:
+        extendedRowWrapper.findByTestId('remove-user').attributes('disabled') === 'disabled',
+      removeUserButtonTooltip: extendedRowWrapper.findByTestId('remove-user-tooltip').exists()
+        ? extendedRowWrapper.findByTestId('remove-user-tooltip').text()
+        : undefined,
+      lastActivityOn: extendedRowWrapper.findByTestId('last_activity_on').text(),
+      lastLoginAt: extendedRowWrapper.findByTestId('last_login_at').text(),
     };
   };
 
@@ -213,7 +218,7 @@ describe('SubscriptionUserList', () => {
     });
 
     describe('when removing a billable user', () => {
-      const [{ user }] = mockTableItems;
+      const { user } = mockTableItems[0];
 
       describe('with billableMemberAsyncDeletion enabled', () => {
         beforeEach(() => {
@@ -273,7 +278,7 @@ describe('SubscriptionUserList', () => {
     });
 
     describe('when the removed billable user is set', () => {
-      const selectedItem = 0;
+      const selectedItem = 1;
       const { user } = mockTableItems[selectedItem];
 
       beforeEach(() => {
@@ -285,7 +290,7 @@ describe('SubscriptionUserList', () => {
       });
 
       it('does not disable unrelated remove button', () => {
-        expect(findAllRemoveUserItems().at(1).attributes().disabled).toBeUndefined();
+        expect(findAllRemoveUserItems().at(2).attributes().disabled).toBeUndefined();
       });
 
       it('shows a tooltip for related users', () => {
@@ -294,8 +299,8 @@ describe('SubscriptionUserList', () => {
         );
       });
 
-      it('does snot show a tooltip for unrelated user', () => {
-        const [, { user: nonRemovedUser }] = mockTableItems;
+      it('does not show a tooltip for unrelated user', () => {
+        const { user: nonRemovedUser } = mockTableItems[2];
 
         expect(findRemoveMemberItem(nonRemovedUser.id).findComponent(GlTooltip).exists()).toBe(
           false,
@@ -322,7 +327,7 @@ describe('SubscriptionUserList', () => {
     });
 
     describe('when the removed billable user is in local storage', () => {
-      const selectedItem = 0;
+      const selectedItem = 1;
       const { user } = mockTableItems[selectedItem];
 
       beforeEach(() => {
@@ -336,7 +341,7 @@ describe('SubscriptionUserList', () => {
       });
 
       it('does not disable unrelated remove button', () => {
-        expect(findAllRemoveUserItems().at(1).attributes().disabled).toBeUndefined();
+        expect(findAllRemoveUserItems().at(3).attributes().disabled).toBeUndefined();
       });
 
       it('shows a tooltip for related users', () => {
@@ -345,8 +350,8 @@ describe('SubscriptionUserList', () => {
         );
       });
 
-      it('does snot show a tooltip for unrelated user', () => {
-        const [, { user: nonRemovedUser }] = mockTableItems;
+      it('does not show a tooltip for unrelated user', () => {
+        const { user: nonRemovedUser } = mockTableItems[2];
 
         expect(findRemoveMemberItem(nonRemovedUser.id).findComponent(GlTooltip).exists()).toBe(
           false,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e34d594479ae1..c4722388e4514 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9547,6 +9547,9 @@ msgstr ""
 msgid "Billing|Awaiting member signup"
 msgstr ""
 
+msgid "Billing|Cannot remove the last owner."
+msgstr ""
+
 msgid "Billing|Cannot remove user"
 msgstr ""
 
-- 
GitLab