mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-17 20:31:50 +01:00
[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 917c969f00
.
---------
Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
parent
642b8d2e6b
commit
e6fce421f5
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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<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);
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>>;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user