SM-281: Secrets Manager Trash (#4730)
* SM-281: Initial commit with trash component setup * SM-281: Customize secrets list component, add ability to hard delete secrets * SM-281: Add support for restoring secrets in SM * SM-281: restoreSecret emit values as an array * SM-281: Fix bug caused by mistake when doing merge conflict resolution * SM-281: Clean up TrashService and move more functionality to TrashApiService * Cleanup responses * Merge TrashService and SecretService * Remove tw-text-sm from dialogs * Split delete into two components * Change secrets table to have a single boolean for trash * SM-281: Rename component to secret-hard-delete * Remove unused organizationId * Remove duplicate buttons --------- Co-authored-by: Hinton <hinton@users.noreply.github.com>
This commit is contained in:
parent
208be8dfbf
commit
d11f03cb78
|
@ -5734,6 +5734,12 @@
|
||||||
"deleteSecrets":{
|
"deleteSecrets":{
|
||||||
"message": "Delete secrets"
|
"message": "Delete secrets"
|
||||||
},
|
},
|
||||||
|
"hardDeleteSecret":{
|
||||||
|
"message": "Permanently delete secret"
|
||||||
|
},
|
||||||
|
"hardDeleteSecrets":{
|
||||||
|
"message": "Permanently delete secrets"
|
||||||
|
},
|
||||||
"secretProjectAssociationDescription" :{
|
"secretProjectAssociationDescription" :{
|
||||||
"message": "Select projects that the secret will be associated with. Only organization users with access to these projects will be able to see the secret."
|
"message": "Select projects that the secret will be associated with. Only organization users with access to these projects will be able to see the secret."
|
||||||
},
|
},
|
||||||
|
@ -5788,6 +5794,9 @@
|
||||||
"secretsNoItemsMessage":{
|
"secretsNoItemsMessage":{
|
||||||
"message": "To get started, add a new secret or import secrets."
|
"message": "To get started, add a new secret or import secrets."
|
||||||
},
|
},
|
||||||
|
"secretsTrashNoItemsMessage":{
|
||||||
|
"message": "There are no secrets in the trash."
|
||||||
|
},
|
||||||
"serviceAccountsNoItemsTitle":{
|
"serviceAccountsNoItemsTitle":{
|
||||||
"message":"Nothing to show yet"
|
"message":"Nothing to show yet"
|
||||||
},
|
},
|
||||||
|
@ -5833,6 +5842,15 @@
|
||||||
"softDeletesSuccessToast":{
|
"softDeletesSuccessToast":{
|
||||||
"message":"Secrets sent to trash"
|
"message":"Secrets sent to trash"
|
||||||
},
|
},
|
||||||
|
"hardDeleteSecretConfirmation": {
|
||||||
|
"message": "Are you sure you want to permanently delete this secret?"
|
||||||
|
},
|
||||||
|
"hardDeleteSecretsConfirmation": {
|
||||||
|
"message": "Are you sure you want to permanently delete these secrets?"
|
||||||
|
},
|
||||||
|
"hardDeletesSuccessToast":{
|
||||||
|
"message":"Secrets permanently deleted"
|
||||||
|
},
|
||||||
"serviceAccountCreated":{
|
"serviceAccountCreated":{
|
||||||
"message":"Service account created"
|
"message":"Service account created"
|
||||||
},
|
},
|
||||||
|
@ -5911,6 +5929,9 @@
|
||||||
"softDeleteSuccessToast":{
|
"softDeleteSuccessToast":{
|
||||||
"message":"Secret sent to trash"
|
"message":"Secret sent to trash"
|
||||||
},
|
},
|
||||||
|
"hardDeleteSuccessToast":{
|
||||||
|
"message":"Secret permanently deleted"
|
||||||
|
},
|
||||||
"searchProjects":{
|
"searchProjects":{
|
||||||
"message":"Search projects"
|
"message":"Search projects"
|
||||||
},
|
},
|
||||||
|
@ -6399,6 +6420,24 @@
|
||||||
"errorReadingImportFile": {
|
"errorReadingImportFile": {
|
||||||
"message": "An error occurred when trying to read the import file"
|
"message": "An error occurred when trying to read the import file"
|
||||||
},
|
},
|
||||||
|
"restoreSecret": {
|
||||||
|
"message": "Restore secret"
|
||||||
|
},
|
||||||
|
"restoreSecrets": {
|
||||||
|
"message": "Restore secrets"
|
||||||
|
},
|
||||||
|
"restoreSecretPrompt": {
|
||||||
|
"message": "Are you sure you want to restore this secret?"
|
||||||
|
},
|
||||||
|
"restoreSecretsPrompt": {
|
||||||
|
"message": "Are you sure you want to restore these secrets?"
|
||||||
|
},
|
||||||
|
"secretRestoredSuccessToast": {
|
||||||
|
"message": "Secret restored"
|
||||||
|
},
|
||||||
|
"secretsRestoredSuccessToast": {
|
||||||
|
"message": "Secrets restored"
|
||||||
|
},
|
||||||
"selectionIsRequired": {
|
"selectionIsRequired": {
|
||||||
"message": "Selection is required."
|
"message": "Selection is required."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<bit-simple-dialog>
|
<bit-simple-dialog>
|
||||||
<span bitDialogTitle>{{ title | i18n }}</span>
|
<span bitDialogTitle>{{ title | i18n }}</span>
|
||||||
<span bitDialogContent class="tw-text-sm">
|
<span bitDialogContent>
|
||||||
<div *ngIf="data.secretIds.length === 1">
|
<div *ngIf="data.secretIds.length === 1">
|
||||||
{{ "softDeleteSecretWarning" | i18n }}
|
{{ "softDeleteSecretWarning" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
</span>
|
</span>
|
||||||
<div bitDialogFooter class="tw-flex tw-gap-2">
|
<div bitDialogFooter class="tw-flex tw-gap-2">
|
||||||
<button type="button" bitButton buttonType="primary" [bitAction]="delete">
|
<button type="button" bitButton buttonType="primary" [bitAction]="delete">
|
||||||
{{ title | i18n }}
|
{{ submitButtonText | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||||
{{ "close" | i18n }}
|
{{ "close" | i18n }}
|
||||||
|
|
|
@ -27,11 +27,15 @@ export class SecretDeleteDialogComponent {
|
||||||
return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets";
|
return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get submitButtonText() {
|
||||||
|
return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets";
|
||||||
|
}
|
||||||
|
|
||||||
delete = async () => {
|
delete = async () => {
|
||||||
await this.secretService.delete(this.data.secretIds);
|
await this.secretService.delete(this.data.secretIds);
|
||||||
this.dialogRef.close(this.data.secretIds);
|
|
||||||
const message =
|
const message =
|
||||||
this.data.secretIds.length === 1 ? "softDeleteSuccessToast" : "softDeletesSuccessToast";
|
this.data.secretIds.length === 1 ? "softDeleteSuccessToast" : "softDeletesSuccessToast";
|
||||||
|
this.dialogRef.close(this.data.secretIds);
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t(message));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t(message));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,42 @@ export class SecretService {
|
||||||
this._secret.next(null);
|
this._secret.next(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTrashedSecrets(organizationId: string): Promise<SecretListView[]> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/secrets/" + organizationId + "/trash",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.createSecretsListView(organizationId, new SecretWithProjectsListResponse(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTrashed(organizationId: string, secretIds: string[]) {
|
||||||
|
await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/secrets/" + organizationId + "/trash/empty",
|
||||||
|
secretIds,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
this._secret.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreTrashed(organizationId: string, secretIds: string[]) {
|
||||||
|
await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/secrets/" + organizationId + "/trash/restore",
|
||||||
|
secretIds,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
this._secret.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||||
return await this.cryptoService.getOrgKey(organizationId);
|
return await this.cryptoService.getOrgKey(organizationId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { SecretsComponent } from "./secrets.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SecretsManagerSharedModule, SecretsRoutingModule],
|
imports: [SecretsManagerSharedModule, SecretsRoutingModule],
|
||||||
declarations: [SecretsComponent, SecretDialogComponent, SecretDeleteDialogComponent],
|
declarations: [SecretDeleteDialogComponent, SecretDialogComponent, SecretsComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
export class SecretsModule {}
|
export class SecretsModule {}
|
||||||
|
|
|
@ -2,20 +2,26 @@
|
||||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<sm-no-items *ngIf="secrets?.length == 0">
|
<ng-container *ngIf="secrets?.length == 0">
|
||||||
<ng-container slot="title">{{ "secretsNoItemsTitle" | i18n }}</ng-container>
|
<sm-no-items *ngIf="trash">
|
||||||
<ng-container slot="description">{{ "secretsNoItemsMessage" | i18n }}</ng-container>
|
<ng-container slot="title">{{ "secretsNoItemsTitle" | i18n }}</ng-container>
|
||||||
<button
|
<ng-container slot="description">{{ "secretsTrashNoItemsMessage" | i18n }}</ng-container>
|
||||||
type="button"
|
</sm-no-items>
|
||||||
slot="button"
|
<sm-no-items *ngIf="!trash">
|
||||||
bitButton
|
<ng-container slot="title">{{ "secretsNoItemsTitle" | i18n }}</ng-container>
|
||||||
buttonType="secondary"
|
<ng-container slot="description">{{ "secretsNoItemsMessage" | i18n }}</ng-container>
|
||||||
(click)="newSecretEvent.emit()"
|
<button
|
||||||
>
|
type="button"
|
||||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
slot="button"
|
||||||
{{ "newSecret" | i18n }}
|
bitButton
|
||||||
</button>
|
buttonType="secondary"
|
||||||
</sm-no-items>
|
(click)="newSecretEvent.emit()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||||
|
{{ "newSecret" | i18n }}
|
||||||
|
</button>
|
||||||
|
</sm-no-items>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<bit-table *ngIf="secrets?.length >= 1" [dataSource]="dataSource">
|
<bit-table *ngIf="secrets?.length >= 1" [dataSource]="dataSource">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
|
@ -86,7 +92,7 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<bit-menu #secretMenu>
|
<bit-menu #secretMenu>
|
||||||
<button type="button" bitMenuItem (click)="editSecretEvent.emit(secret.id)">
|
<button type="button" bitMenuItem (click)="editSecretEvent.emit(secret.id)" *ngIf="!trash">
|
||||||
<i class="bwi bwi-fw bwi-pencil tw-text-xl" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-pencil tw-text-xl" aria-hidden="true"></i>
|
||||||
{{ "editSecret" | i18n }}
|
{{ "editSecret" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -98,9 +104,14 @@
|
||||||
<i class="bwi bwi-fw bwi-clone tw-text-xl" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-clone tw-text-xl" aria-hidden="true"></i>
|
||||||
{{ "copySecretValue" | i18n }}
|
{{ "copySecretValue" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem (click)="projectsEvent.emit(secret.id)">
|
<button
|
||||||
<i class="bwi bwi-fw bwi-sitemap tw-text-xl" aria-hidden="true"></i>
|
type="button"
|
||||||
{{ "projects" | i18n }}
|
bitMenuItem
|
||||||
|
(click)="restoreSecretsEvent.emit([secret.id])"
|
||||||
|
*ngIf="trash"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-refresh tw-text-xl" aria-hidden="true"></i>
|
||||||
|
{{ "restoreSecret" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem (click)="deleteSecretsEvent.emit([secret.id])">
|
<button type="button" bitMenuItem (click)="deleteSecretsEvent.emit([secret.id])">
|
||||||
<i class="bwi bwi-fw bwi-trash tw-text-xl tw-text-danger" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash tw-text-xl tw-text-danger" aria-hidden="true"></i>
|
||||||
|
@ -112,9 +123,9 @@
|
||||||
</bit-table>
|
</bit-table>
|
||||||
|
|
||||||
<bit-menu #tableMenu>
|
<bit-menu #tableMenu>
|
||||||
<button type="button" bitMenuItem>
|
<button type="button" bitMenuItem (click)="bulkRestoreSecrets()" *ngIf="trash">
|
||||||
<i class="bwi bwi-fw bwi-sitemap tw-text-xl" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-refresh tw-text-xl" aria-hidden="true"></i>
|
||||||
{{ "projects" | i18n }}
|
<span>{{ "restoreSelected" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitMenuItem (click)="bulkDeleteSecrets()">
|
<button type="button" bitMenuItem (click)="bulkDeleteSecrets()">
|
||||||
<i class="bwi bwi-fw bwi-trash tw-text-xl tw-text-danger" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash tw-text-xl tw-text-danger" aria-hidden="true"></i>
|
||||||
|
|
|
@ -24,13 +24,15 @@ export class SecretsListComponent implements OnDestroy {
|
||||||
}
|
}
|
||||||
private _secrets: SecretListView[];
|
private _secrets: SecretListView[];
|
||||||
|
|
||||||
|
@Input() trash: boolean;
|
||||||
|
|
||||||
@Output() editSecretEvent = new EventEmitter<string>();
|
@Output() editSecretEvent = new EventEmitter<string>();
|
||||||
@Output() copySecretNameEvent = new EventEmitter<string>();
|
@Output() copySecretNameEvent = new EventEmitter<string>();
|
||||||
@Output() copySecretValueEvent = new EventEmitter<string>();
|
@Output() copySecretValueEvent = new EventEmitter<string>();
|
||||||
@Output() projectsEvent = new EventEmitter<string>();
|
|
||||||
@Output() onSecretCheckedEvent = new EventEmitter<string[]>();
|
@Output() onSecretCheckedEvent = new EventEmitter<string[]>();
|
||||||
@Output() deleteSecretsEvent = new EventEmitter<string[]>();
|
@Output() deleteSecretsEvent = new EventEmitter<string[]>();
|
||||||
@Output() newSecretEvent = new EventEmitter();
|
@Output() newSecretEvent = new EventEmitter();
|
||||||
|
@Output() restoreSecretsEvent = new EventEmitter();
|
||||||
|
|
||||||
private destroy$: Subject<void> = new Subject<void>();
|
private destroy$: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
|
@ -64,4 +66,10 @@ export class SecretsListComponent implements OnDestroy {
|
||||||
this.deleteSecretsEvent.emit(this.selection.selected);
|
this.deleteSecretsEvent.emit(this.selection.selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bulkRestoreSecrets() {
|
||||||
|
if (this.selection.selected.length >= 1) {
|
||||||
|
this.restoreSecretsEvent.emit(this.selection.selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { SecretsModule } from "./secrets/secrets.module";
|
||||||
import { ServiceAccountsModule } from "./service-accounts/service-accounts.module";
|
import { ServiceAccountsModule } from "./service-accounts/service-accounts.module";
|
||||||
import { SettingsModule } from "./settings/settings.module";
|
import { SettingsModule } from "./settings/settings.module";
|
||||||
import { SMGuard } from "./sm.guard";
|
import { SMGuard } from "./sm.guard";
|
||||||
|
import { TrashModule } from "./trash/trash.module";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
buildFlaggedRoute("secretsManager", {
|
buildFlaggedRoute("secretsManager", {
|
||||||
|
@ -49,6 +50,13 @@ const routes: Routes = [
|
||||||
titleId: "serviceAccounts",
|
titleId: "serviceAccounts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "trash",
|
||||||
|
loadChildren: () => TrashModule,
|
||||||
|
data: {
|
||||||
|
titleId: "trash",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "settings",
|
path: "settings",
|
||||||
loadChildren: () => SettingsModule,
|
loadChildren: () => SettingsModule,
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<bit-simple-dialog>
|
||||||
|
<span bitDialogTitle>{{ title | i18n }}</span>
|
||||||
|
<span bitDialogContent>
|
||||||
|
{{
|
||||||
|
data.secretIds.length === 1
|
||||||
|
? ("hardDeleteSecretConfirmation" | i18n)
|
||||||
|
: ("hardDeleteSecretsConfirmation" | i18n)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<div bitDialogFooter class="tw-flex tw-gap-2">
|
||||||
|
<button type="button" bitButton buttonType="primary" [bitAction]="delete">
|
||||||
|
{{ submitButtonText | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bit-simple-dialog>
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
|
import { SecretService } from "../../secrets/secret.service";
|
||||||
|
|
||||||
|
export interface SecretHardDeleteOperation {
|
||||||
|
secretIds: string[];
|
||||||
|
organizationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "sm-secret-hard-delete-dialog",
|
||||||
|
templateUrl: "./secret-hard-delete.component.html",
|
||||||
|
})
|
||||||
|
export class SecretHardDeleteDialogComponent {
|
||||||
|
constructor(
|
||||||
|
public dialogRef: DialogRef,
|
||||||
|
private secretService: SecretService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@Inject(DIALOG_DATA) public data: SecretHardDeleteOperation
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.data.secretIds.length === 1 ? "hardDeleteSecret" : "hardDeleteSecrets";
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButtonText() {
|
||||||
|
return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets";
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async () => {
|
||||||
|
await this.secretService.deleteTrashed(this.data.organizationId, this.data.secretIds);
|
||||||
|
const message =
|
||||||
|
this.data.secretIds.length === 1 ? "hardDeleteSuccessToast" : "hardDeletesSuccessToast";
|
||||||
|
this.dialogRef.close(this.data.secretIds);
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t(message));
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<bit-simple-dialog>
|
||||||
|
<span bitDialogTitle>{{ title | i18n }}</span>
|
||||||
|
<span bitDialogContent>
|
||||||
|
{{
|
||||||
|
data.secretIds.length === 1 ? ("restoreSecretPrompt" | i18n) : ("restoreSecretsPrompt" | i18n)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<div bitDialogFooter class="tw-flex tw-gap-2">
|
||||||
|
<button type="button" bitButton buttonType="primary" [bitAction]="restore">
|
||||||
|
{{ title | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bit-simple-dialog>
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
|
import { SecretService } from "../../secrets/secret.service";
|
||||||
|
|
||||||
|
export interface SecretRestoreOperation {
|
||||||
|
secretIds: string[];
|
||||||
|
organizationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "sm-secret-restore-dialog",
|
||||||
|
templateUrl: "./secret-restore.component.html",
|
||||||
|
})
|
||||||
|
export class SecretRestoreDialogComponent {
|
||||||
|
constructor(
|
||||||
|
public dialogRef: DialogRef,
|
||||||
|
private secretService: SecretService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@Inject(DIALOG_DATA) public data: SecretRestoreOperation
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.data.secretIds.length === 1 ? "restoreSecret" : "restoreSecrets";
|
||||||
|
}
|
||||||
|
|
||||||
|
restore = async () => {
|
||||||
|
let message = "";
|
||||||
|
await this.secretService.restoreTrashed(this.data.organizationId, this.data.secretIds);
|
||||||
|
message =
|
||||||
|
this.data.secretIds.length === 1
|
||||||
|
? "secretRestoredSuccessToast"
|
||||||
|
: "secretsRestoredSuccessToast";
|
||||||
|
this.dialogRef.close(this.data.secretIds);
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t(message));
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { TrashComponent } from "./trash.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: TrashComponent,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class TrashRoutingModule {}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<sm-header>
|
||||||
|
<sm-new-menu></sm-new-menu>
|
||||||
|
</sm-header>
|
||||||
|
<sm-secrets-list
|
||||||
|
(deleteSecretsEvent)="openDeleteSecret($event)"
|
||||||
|
(restoreSecretsEvent)="openRestoreSecret($event)"
|
||||||
|
[secrets]="secrets$ | async"
|
||||||
|
[trash]="true"
|
||||||
|
></sm-secrets-list>
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { combineLatestWith, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { SecretListView } from "../models/view/secret-list.view";
|
||||||
|
import { SecretService } from "../secrets/secret.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SecretHardDeleteDialogComponent,
|
||||||
|
SecretHardDeleteOperation,
|
||||||
|
} from "./dialog/secret-hard-delete.component";
|
||||||
|
import {
|
||||||
|
SecretRestoreDialogComponent,
|
||||||
|
SecretRestoreOperation,
|
||||||
|
} from "./dialog/secret-restore.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "sm-trash",
|
||||||
|
templateUrl: "./trash.component.html",
|
||||||
|
})
|
||||||
|
export class TrashComponent implements OnInit {
|
||||||
|
secrets$: Observable<SecretListView[]>;
|
||||||
|
|
||||||
|
private organizationId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private secretService: SecretService,
|
||||||
|
private dialogService: DialogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.secrets$ = this.secretService.secret$.pipe(
|
||||||
|
startWith(null),
|
||||||
|
combineLatestWith(this.route.params),
|
||||||
|
switchMap(async ([_, params]) => {
|
||||||
|
this.organizationId = params.organizationId;
|
||||||
|
return await this.getSecrets();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSecrets(): Promise<SecretListView[]> {
|
||||||
|
return await this.secretService.getTrashedSecrets(this.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteSecret(secretIds: string[]) {
|
||||||
|
this.dialogService.open<unknown, SecretHardDeleteOperation>(SecretHardDeleteDialogComponent, {
|
||||||
|
data: {
|
||||||
|
secretIds: secretIds,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openRestoreSecret(secretIds: string[]) {
|
||||||
|
this.dialogService.open<unknown, SecretRestoreOperation>(SecretRestoreDialogComponent, {
|
||||||
|
data: {
|
||||||
|
secretIds: secretIds,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||||
|
|
||||||
|
import { SecretHardDeleteDialogComponent } from "./dialog/secret-hard-delete.component";
|
||||||
|
import { SecretRestoreDialogComponent } from "./dialog/secret-restore.component";
|
||||||
|
import { TrashRoutingModule } from "./trash-routing.module";
|
||||||
|
import { TrashComponent } from "./trash.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SecretsManagerSharedModule, TrashRoutingModule],
|
||||||
|
declarations: [SecretHardDeleteDialogComponent, SecretRestoreDialogComponent, TrashComponent],
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
export class TrashModule {}
|
Loading…
Reference in New Issue