[SM-249] Adding detailed description of which secrets are unable to be deleted (#4849)

* Adding detailed description of which secrets are unable to be deleted

* removing accidental change

* removing Can write from Service Account options

* suggested PR changes

* thomas's suggested changes

* fixing merge conflicts after merging in master
This commit is contained in:
cd-bitwarden 2023-03-07 15:33:39 -05:00 committed by GitHub
parent 9d4d340930
commit 1d01bfbb61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 80 additions and 33 deletions

View File

@ -254,10 +254,10 @@ export class OverviewComponent implements OnInit, OnDestroy {
});
}
openDeleteSecret(secretIds: string[]) {
openDeleteSecret(event: SecretListView[]) {
this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, {
data: {
secretIds: secretIds,
secrets: event,
},
});
}

View File

@ -62,10 +62,10 @@ export class ProjectSecretsComponent {
});
}
openDeleteSecret(secretIds: string[]) {
openDeleteSecret(event: SecretListView[]) {
this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, {
data: {
secretIds: secretIds,
secrets: event,
},
});
}

View File

@ -1,7 +1,7 @@
<bit-simple-dialog>
<span bitDialogTitle>{{ title | i18n }}</span>
<span bitDialogContent>
<div *ngIf="data.secretIds.length === 1">
<div *ngIf="showSoftDeleteSecretWarning">
{{ "softDeleteSecretWarning" | i18n }}
</div>
{{ "deleteItemConfirmation" | i18n }}

View File

@ -3,11 +3,18 @@ import { Component, Inject } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { DialogService } from "@bitwarden/components";
import { SecretListView } from "../../models/view/secret-list.view";
import {
BulkOperationStatus,
BulkStatusDetails,
BulkStatusDialogComponent,
} from "../../shared/dialogs/bulk-status-dialog.component";
import { SecretService } from "../secret.service";
export interface SecretDeleteOperation {
secretIds: string[];
secrets: SecretListView[];
}
@Component({
@ -20,22 +27,45 @@ export class SecretDeleteDialogComponent {
private secretService: SecretService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
@Inject(DIALOG_DATA) public data: SecretDeleteOperation
@Inject(DIALOG_DATA) private data: SecretDeleteOperation,
private dialogService: DialogService
) {}
showSoftDeleteSecretWarning = this.data.secrets.length === 1;
get title() {
return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets";
return this.data.secrets.length === 1 ? "deleteSecret" : "deleteSecrets";
}
get submitButtonText() {
return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets";
return this.data.secrets.length === 1 ? "deleteSecret" : "deleteSecrets";
}
delete = async () => {
await this.secretService.delete(this.data.secretIds);
const bulkResponses = await this.secretService.delete(this.data.secrets);
if (bulkResponses.find((response) => response.errorMessage)) {
this.openBulkStatusDialog(bulkResponses.filter((response) => response.errorMessage));
this.dialogRef.close();
return;
}
const message =
this.data.secretIds.length === 1 ? "softDeleteSuccessToast" : "softDeletesSuccessToast";
this.dialogRef.close(this.data.secretIds);
this.data.secrets.length === 1 ? "softDeleteSuccessToast" : "softDeletesSuccessToast";
this.platformUtilsService.showToast("success", null, this.i18nService.t(message));
this.dialogRef.close();
};
openBulkStatusDialog(bulkStatusResults: BulkOperationStatus[]) {
this.dialogService.open<unknown, BulkStatusDetails>(BulkStatusDialogComponent, {
data: {
title: "deleteSecrets",
subTitle: "secrets",
columnTitle: "secretName",
message: "bulkDeleteSecretsErrorMessage",
details: bulkStatusResults,
},
});
}
}

View File

@ -8,6 +8,8 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { DialogService } from "@bitwarden/components";
import { ProjectListView } from "../../models/view/project-list.view";
import { SecretListView } from "../../models/view/secret-list.view";
import { SecretProjectView } from "../../models/view/secret-project.view";
import { SecretView } from "../../models/view/secret.view";
import { ProjectService } from "../../projects/project.service";
import { SecretService } from "../secret.service";
@ -112,11 +114,13 @@ export class SecretDialogComponent implements OnInit {
}
protected openDeleteSecretDialog() {
const secretListView: SecretListView[] = this.getSecretListView();
const dialogRef = this.dialogService.open<unknown, SecretDeleteOperation>(
SecretDeleteDialogComponent,
{
data: {
secretIds: [this.data.secretId],
secrets: secretListView,
},
}
);
@ -146,4 +150,17 @@ export class SecretDialogComponent implements OnInit {
secretView.projects = [this.projects.find((p) => p.id == this.formGroup.value.project)];
return secretView;
}
private getSecretListView() {
const secretListViews: SecretListView[] = [];
const emptyProjects: SecretProjectView[] = [];
const selectedProject = [this.projects.find((p) => p.id == this.formGroup.value.project)];
const secretListView = new SecretListView();
secretListView.organizationId = this.data.organizationId;
secretListView.name = this.formGroup.value.name;
secretListView.projects = selectedProject ? selectedProject : emptyProjects;
secretListViews.push(secretListView);
return secretListViews;
}
}

View File

@ -10,6 +10,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr
import { SecretListView } from "../models/view/secret-list.view";
import { SecretProjectView } from "../models/view/secret-project.view";
import { SecretView } from "../models/view/secret.view";
import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component";
import { SecretRequest } from "./requests/secret.request";
import { SecretListItemResponse } from "./responses/secret-list-item.response";
@ -82,23 +83,18 @@ export class SecretService {
this._secret.next(await this.createSecretView(new SecretResponse(r)));
}
async delete(secretIds: string[]) {
async delete(secrets: SecretListView[]): Promise<BulkOperationStatus[]> {
const secretIds = secrets.map((secret) => secret.id);
const r = await this.apiService.send("POST", "/secrets/delete", secretIds, true, true);
const responseErrors: string[] = [];
r.data.forEach((element: { error: string }) => {
if (element.error) {
responseErrors.push(element.error);
}
});
// TODO waiting to hear back on how to display multiple errors.
// for now send as a list of strings to be displayed in toast.
if (responseErrors?.length >= 1) {
throw new Error(responseErrors.join(","));
}
this._secret.next(null);
return r.data.map((element: { id: string; error: string }) => {
const bulkOperationStatus = new BulkOperationStatus();
bulkOperationStatus.id = element.id;
bulkOperationStatus.name = secrets.find((secret) => secret.id == element.id).name;
bulkOperationStatus.errorMessage = element.error;
return bulkOperationStatus;
});
}
async getTrashedSecrets(organizationId: string): Promise<SecretListView[]> {

View File

@ -66,10 +66,10 @@ export class SecretsComponent implements OnInit {
});
}
openDeleteSecret(secretIds: string[]) {
openDeleteSecret(event: SecretListView[]) {
this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, {
data: {
secretIds: secretIds,
secrets: event,
},
});
}

View File

@ -124,7 +124,7 @@
<i class="bwi bwi-fw bwi-refresh" aria-hidden="true"></i>
{{ "restoreSecret" | i18n }}
</button>
<button type="button" bitMenuItem (click)="deleteSecretsEvent.emit([secret.id])">
<button type="button" bitMenuItem (click)="deleteSecretsEvent.emit([secret])">
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
<span class="tw-text-danger">{{
(trash ? "permanentlyDelete" : "deleteSecret") | i18n

View File

@ -35,7 +35,7 @@ export class SecretsListComponent implements OnDestroy {
@Output() copySecretNameEvent = new EventEmitter<string>();
@Output() copySecretValueEvent = new EventEmitter<string>();
@Output() onSecretCheckedEvent = new EventEmitter<string[]>();
@Output() deleteSecretsEvent = new EventEmitter<string[]>();
@Output() deleteSecretsEvent = new EventEmitter<SecretListView[]>();
@Output() newSecretEvent = new EventEmitter();
@Output() restoreSecretsEvent = new EventEmitter();
@ -68,7 +68,9 @@ export class SecretsListComponent implements OnDestroy {
bulkDeleteSecrets() {
if (this.selection.selected.length >= 1) {
this.deleteSecretsEvent.emit(this.selection.selected);
this.deleteSecretsEvent.emit(
this.secrets.filter((secret) => this.selection.isSelected(secret.id))
);
}
}

View File

@ -46,7 +46,9 @@ export class TrashComponent implements OnInit {
return await this.secretService.getTrashedSecrets(this.organizationId);
}
openDeleteSecret(secretIds: string[]) {
openDeleteSecret(secrets: SecretListView[]) {
const secretIds = secrets.map((secret) => secret.id);
this.dialogService.open<unknown, SecretHardDeleteOperation>(SecretHardDeleteDialogComponent, {
data: {
secretIds: secretIds,