mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-28 22:21:35 +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 }}
|
{{ "remove" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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>
|
</bit-menu>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -61,6 +61,7 @@ import { OrganizationUserView } from "../core/views/organization-user.view";
|
|||||||
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
||||||
|
|
||||||
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.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 { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
|
||||||
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
|
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
|
||||||
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
||||||
@ -543,6 +544,21 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
await this.load();
|
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() {
|
async bulkRevoke() {
|
||||||
await this.bulkRevokeOrRestore(true);
|
await this.bulkRevokeOrRestore(true);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { LooseComponentsModule } from "../../../shared";
|
|||||||
import { SharedOrganizationModule } from "../shared";
|
import { SharedOrganizationModule } from "../shared";
|
||||||
|
|
||||||
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.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 { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
|
||||||
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
|
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
|
||||||
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
||||||
@ -35,6 +36,7 @@ import { MembersComponent } from "./members.component";
|
|||||||
BulkStatusComponent,
|
BulkStatusComponent,
|
||||||
MembersComponent,
|
MembersComponent,
|
||||||
ResetPasswordComponent,
|
ResetPasswordComponent,
|
||||||
|
BulkDeleteDialogComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MembersModule {}
|
export class MembersModule {}
|
||||||
|
@ -9695,5 +9695,14 @@
|
|||||||
},
|
},
|
||||||
"suspendedOwnerOrgMessage": {
|
"suspendedOwnerOrgMessage": {
|
||||||
"message": "To regain access to your organization, add a payment method."
|
"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
|
* @param id - Organization user identifier
|
||||||
*/
|
*/
|
||||||
abstract deleteOrganizationUser(organizationId: string, id: string): Promise<void>;
|
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,
|
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