From e6fce421f574678a0476e58e69589dce4e4ae10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:49:16 +0000 Subject: [PATCH] [PM-10324] Add bulk delete option for organization members (#11892) * Refactor organization user API service to support bulk deletion of users * Add copy for bulk user delete dialog * Add bulk user delete dialog component * Add bulk user delete functionality to members component * Refactor members component to only display bulk user deletion option if the Account Deprovisioning flag is enabled * Patch build process * Revert "Patch build process" This reverts commit 917c969f004274d90e8c35ecf5fa83085a473f95. --------- Co-authored-by: Matt Bishop --- .../bulk/bulk-delete-dialog.component.html | 85 +++++++++++++++++++ .../bulk/bulk-delete-dialog.component.ts | 65 ++++++++++++++ .../members/members.component.html | 11 +++ .../members/members.component.ts | 16 ++++ .../organizations/members/members.module.ts | 2 + apps/web/src/locales/en/messages.json | 9 ++ .../organization-user-api.service.ts | 11 +++ .../default-organization-user-api.service.ts | 14 +++ 8 files changed, 213 insertions(+) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html new file mode 100644 index 00000000000..9a4ce89671e --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html @@ -0,0 +1,85 @@ + + + + {{ "noSelectedMembersApplicable" | i18n }} + + + {{ error }} + + + +

{{ "deleteOrganizationUserWarning" | i18n }}

+
+ + + + {{ "member" | i18n }} + + + + + + + + +
+ {{ user.email }} + + {{ "invited" | i18n }} + +
+ {{ user.name }} + + +
+
+
+ + + + + {{ "member" | i18n }} + {{ "status" | i18n }} + + + + + + + + + {{ user.email }} + {{ user.name }} + + + {{ statuses.get(user.id) }} + + + {{ "bulkFilteredMessage" | i18n }} + + + + + +
+ + + + +
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts new file mode 100644 index 00000000000..1755b0b0b91 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts @@ -0,0 +1,65 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +import { BulkUserDetails } from "./bulk-status.component"; + +type BulkDeleteDialogParams = { + organizationId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: "bulk-delete-dialog.component.html", +}) +export class BulkDeleteDialogComponent { + organizationId: string; + users: BulkUserDetails[]; + loading = false; + done = false; + error: string = null; + statuses = new Map(); + userStatusType = OrganizationUserStatusType; + + constructor( + @Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams, + protected i18nService: I18nService, + private organizationUserApiService: OrganizationUserApiService, + ) { + this.organizationId = dialogParams.organizationId; + this.users = dialogParams.users; + } + + async submit() { + try { + this.loading = true; + this.error = null; + + const response = await this.organizationUserApiService.deleteManyOrganizationUsers( + this.organizationId, + this.users.map((user) => user.id), + ); + + response.data.forEach((entry) => { + this.statuses.set( + entry.id, + entry.error ? entry.error : this.i18nService.t("deletedSuccessfully"), + ); + }); + + this.done = true; + } catch (e) { + this.error = e.message; + } finally { + this.loading = false; + } + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(BulkDeleteDialogComponent, config); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index f87934dbe81..a9c5ab3e4a8 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -137,6 +137,17 @@ {{ "remove" | i18n }} + diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 30c5106a4fa..6d2d3a45128 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -61,6 +61,7 @@ import { OrganizationUserView } from "../core/views/organization-user.view"; import { openEntityEventsDialog } from "../manage/entity-events.component"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; +import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; @@ -543,6 +544,21 @@ export class MembersComponent extends BaseMembersComponent await this.load(); } + async bulkDelete() { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, { + data: { + organizationId: this.organization.id, + users: this.dataSource.getCheckedUsers(), + }, + }); + await lastValueFrom(dialogRef.closed); + await this.load(); + } + async bulkRevoke() { await this.bulkRevokeOrRestore(true); } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index d7c5a9bf1df..81697f8c845 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -8,6 +8,7 @@ import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; +import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; @@ -35,6 +36,7 @@ import { MembersComponent } from "./members.component"; BulkStatusComponent, MembersComponent, ResetPasswordComponent, + BulkDeleteDialogComponent, ], }) export class MembersModule {} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 00d2102c786..76cd45e1498 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9695,5 +9695,14 @@ }, "suspendedOwnerOrgMessage": { "message": "To regain access to your organization, add a payment method." + }, + "deleteMembers": { + "message": "Delete members" + }, + "noSelectedMembersApplicable": { + "message": "This action is not applicable to any of the selected members." + }, + "deletedSuccessfully": { + "message": "Deleted successfully" } } diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index 42cbe1438d1..3186bdaa84b 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -282,4 +282,15 @@ export abstract class OrganizationUserApiService { * @param id - Organization user identifier */ abstract deleteOrganizationUser(organizationId: string, id: string): Promise; + + /** + * Delete many organization users + * @param organizationId - Identifier for the organization the users belongs to + * @param ids - List of organization user identifiers to delete + * @return List of user ids, including both those that were successfully deleted and those that had an error + */ + abstract deleteManyOrganizationUsers( + organizationId: string, + ids: string[], + ): Promise>; } diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index d9e069dc934..7289f41d7e7 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -369,4 +369,18 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer false, ); } + + async deleteManyOrganizationUsers( + organizationId: string, + ids: string[], + ): Promise> { + const r = await this.apiService.send( + "DELETE", + "/organizations/" + organizationId + "/users/delete-account", + new OrganizationUserBulkRequest(ids), + true, + true, + ); + return new ListResponse(r, OrganizationUserBulkResponse); + } }