Skip to content

Commit

Permalink
[PM-10324] Add bulk delete option for organization members (#11892)
Browse files Browse the repository at this point in the history
* 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 917c969.

---------

Co-authored-by: Matt Bishop <[email protected]>
  • Loading branch information
r-tome and withinfocus authored Nov 14, 2024
1 parent 642b8d2 commit e6fce42
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<bit-dialog dialogSize="large" [title]="'deleteMembers' | i18n">
<ng-container bitDialogContent>
<bit-callout type="danger" *ngIf="users.length <= 0">
{{ "noSelectedMembersApplicable" | i18n }}
</bit-callout>
<bit-callout type="danger" [title]="'error' | i18n" *ngIf="error">
{{ error }}
</bit-callout>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error">
<p bitTypography="body1">{{ "deleteOrganizationUserWarning" | i18n }}</p>
</bit-callout>
<bit-table>
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "member" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell class="tw-w-5">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</td>
<td bitCell>
<div>
{{ user.email }}
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="user.status === this.userStatusType.Invited"
>
{{ "invited" | i18n }}
</span>
</div>
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
<ng-container *ngIf="done">
<bit-table>
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell class="tw-w-5">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</td>
<td bitCell>
{{ user.email }}
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
</td>
<td *ngIf="statuses.has(user.id)" bitCell>
{{ statuses.get(user.id) }}
</td>
<td *ngIf="!statuses.has(user.id)" bitCell>
{{ "bulkFilteredMessage" | i18n }}
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
</ng-container>
<ng-container bitDialogFooter>
<button
*ngIf="!done && users.length > 0"
bitButton
type="submit"
buttonType="primary"
[disabled]="loading"
(click)="submit()"
>
{{ "deleteMembers" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>
Original file line number Diff line number Diff line change
@@ -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<string, string>();
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<BulkDeleteDialogParams>) {
return dialogService.open(BulkDeleteDialogComponent, config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@
{{ "remove" | i18n }}
</span>
</button>
<button
*ngIf="accountDeprovisioningEnabled$ | async"
type="button"
bitMenuItem
(click)="bulkDelete()"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-trash"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu>
</th>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -543,6 +544,21 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -35,6 +36,7 @@ import { MembersComponent } from "./members.component";
BulkStatusComponent,
MembersComponent,
ResetPasswordComponent,
BulkDeleteDialogComponent,
],
})
export class MembersModule {}
9 changes: 9 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,15 @@ export abstract class OrganizationUserApiService {
* @param id - Organization user identifier
*/
abstract deleteOrganizationUser(organizationId: string, id: string): Promise<void>;

/**
* 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<ListResponse<OrganizationUserBulkResponse>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,18 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer
false,
);
}

async deleteManyOrganizationUsers(
organizationId: string,
ids: string[],
): Promise<ListResponse<OrganizationUserBulkResponse>> {
const r = await this.apiService.send(
"DELETE",
"/organizations/" + organizationId + "/users/delete-account",
new OrganizationUserBulkRequest(ids),
true,
true,
);
return new ListResponse(r, OrganizationUserBulkResponse);
}
}

0 comments on commit e6fce42

Please sign in to comment.