Skip to content

Commit

Permalink
Add console bulk delete
Browse files Browse the repository at this point in the history
  • Loading branch information
ptkach committed Jan 16, 2025
1 parent 348cebf commit d19f2b1
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 28 deletions.
8 changes: 7 additions & 1 deletion console-webapp/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import { BackendService } from './shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import {
DomainListComponent,
ReasonDialogComponent,
ResponseDialogComponent,
} from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
Expand Down Expand Up @@ -92,6 +96,8 @@ export class SelectedRegistrarModule {}
TldsComponent,
WhoisComponent,
WhoisEditComponent,
ReasonDialogComponent,
ResponseDialogComponent,
],
bootstrap: [AppComponent],
imports: [
Expand Down
52 changes: 38 additions & 14 deletions console-webapp/src/app/domains/domainList.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,22 @@ <h1>No domains found</h1>
/>
</mat-form-field>

<div class="console-app__domains-selection" [ngClass]="{'active': selection.hasValue()}">
<div class="console-app__domains-selection-text">{{selection.selected.length}} Selected</div>
<div class="console-app__domains-selection-actions"></div>
<div
class="console-app__domains-selection"
[ngClass]="{ active: selection.hasValue() }"
>
<div class="console-app__domains-selection-text">
{{ selection.selected.length }} Selected
</div>
<div class="console-app__domains-selection-actions">
<button
mat-flat-button
aria-label="Delete Selected Domains"
(click)="deleteSelectedDomains()"
>
Delete Selected Domains
</button>
</div>
</div>

<mat-table
Expand All @@ -78,26 +91,37 @@ <h1>No domains found</h1>
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected"
[indeterminate]="selection.hasValue() && !isAllSelected"
[aria-label]="checkboxLabel()">
<mat-checkbox
(change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected"
[indeterminate]="selection.hasValue() && !isAllSelected"
[aria-label]="checkboxLabel()"
>
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)"
>
</mat-checkbox>
</mat-cell>
</ng-container>

<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.domainName
}}</mat-cell>
<mat-cell *matCellDef="let element">
<mat-icon
*ngIf="getOperationMessage(element.domainName)"
[matTooltip]="getOperationMessage(element.domainName)"
matTooltipPosition="above"
class="primary-text"
>info</mat-icon
>
<span>{{ element.domainName }}</span>
</mat-cell>
</ng-container>

<ng-container matColumnDef="creationTime">
Expand Down
21 changes: 18 additions & 3 deletions console-webapp/src/app/domains/domainList.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
}

&__domains-selection {
height: 30px;
height: 60px;
max-height: 0;
transition: max-height .2s linear;
transition: max-height 0.2s linear;
display: flex;
align-items: center;
overflow: hidden;
gap: 20px;
&-text {
font-weight: bold;
}
&.active {
max-height: 30px;
max-height: 60px;
}
}

Expand Down Expand Up @@ -54,6 +61,14 @@
max-width: 60px;
padding-left: 15px;
}
.mat-column-domainName {
position: relative;
padding-left: 25px;
mat-icon {
position: absolute;
left: 0;
}
}
}

&__domains-spinner {
Expand Down
146 changes: 142 additions & 4 deletions console-webapp/src/app/domains/domainList.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,88 @@

import { SelectionModel } from '@angular/cdk/collections';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Component, ViewChild, effect } from '@angular/core';
import { Component, ViewChild, effect, Inject } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { Subject, debounceTime, take, filter } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';

interface DomainResponse {
message: string;
responseCode: string;
}

interface DomainData {
[domain: string]: DomainResponse;
}

@Component({
selector: 'app-response-dialog',
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.content" />
<mat-dialog-actions>
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
`,
})
export class ResponseDialogComponent {
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { title: string; content: string }
) {}

onClose(): void {
this.dialogRef.close();
}
}

@Component({
selector: 'app-reason-dialog',
template: `
<h2 mat-dialog-title>
Please provide a reason for {{ data.operation }} the domain(s):
</h2>
<mat-dialog-content>
<mat-form-field appearance="outline" style="width:100%">
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onDelete()" [disabled]="!reason">
Delete
</button>
</mat-dialog-actions>
`,
})
export class ReasonDialogComponent {
reason: string = '';

constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { operation: 'deleting' | 'suspending' }
) {}

onDelete(): void {
this.dialogRef.close(this.reason);
}

onCancel(): void {
this.dialogRef.close();
}
}

@Component({
selector: 'app-domain-list',
Expand Down Expand Up @@ -55,13 +128,18 @@ export class DomainListComponent {
resultsPerPage = 50;
totalResults?: number = 0;

reason: string = '';

operationResult: DomainData | undefined;

@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;

constructor(
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar
private _snackBar: MatSnackBar,
private dialog: MatDialog
) {
effect(() => {
this.pageNumber = 0;
Expand Down Expand Up @@ -138,6 +216,7 @@ export class DomainListComponent {
onPageChange(event: PageEvent) {
this.pageNumber = event.pageIndex;
this.resultsPerPage = event.pageSize;
this.selection.clear();
this.reloadData();
}

Expand All @@ -156,7 +235,9 @@ export class DomainListComponent {
if (!row) {
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
}
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.domainName + 1}`;
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
row.domainName
}`;
}

private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
Expand All @@ -168,4 +249,61 @@ export class DomainListComponent {
return this.isAllSelected || o1.domainName === o2.domainName;
};
}

getOperationMessage(domain: string) {
if (this.operationResult && this.operationResult[domain])
return this.operationResult[domain].message;
return '';
}

sendDeleteRequest(reason: string) {
this.isLoading = true;
this.domainListService
.deleteDomains(
this.selection.selected,
reason,
this.registrarService.registrarId()
)
.pipe(take(1))
.subscribe({
next: (result: DomainData) => {
this.isLoading = false;
const successCount = Object.keys(result).filter((domainName) =>
result[domainName].responseCode.toString().startsWith('1')
).length;
const failureCount = Object.keys(result).length - successCount;
this.dialog.open(ResponseDialogComponent, {
data: {
title: 'Domain Deletion Results',
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
failureCount
? 'Some domains could not be deleted due to ongoing processes or server errors. '
: ''
}Please check the table for more information.`,
},
});
this.selection.clear();
this.operationResult = result;
this.reloadData();
},
error: (err: HttpErrorResponse) =>
this._snackBar.open(err.error || err.message),
});
}

deleteSelectedDomains() {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: 'deleting',
},
});

dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe(this.sendDeleteRequest.bind(this));
}
}
10 changes: 9 additions & 1 deletion console-webapp/src/app/domains/domainList.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class DomainListService {
private backendService: BackendService,
private registrarService: RegistrarService
) {}

retrieveDomains(
pageNumber?: number,
resultsPerPage?: number,
Expand All @@ -71,4 +70,13 @@ export class DomainListService {
})
);
}

deleteDomains(domains: Domain[], reason: string, registrarId: string) {
return this.backendService.bulkDomainAction(
domains.map((d) => d.domainName),
reason,
'DELETE',
registrarId
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[ngModelOptions]="{ standalone: true }"
(focus)="onFocus()"
[matAutocomplete]="auto"
spellcheck="false"
/>
<mat-autocomplete
autoActiveFirstOption
Expand Down
17 changes: 17 additions & 0 deletions console-webapp/src/app/shared/services/backend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,23 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}

bulkDomainAction(
domainNames: string[],
reason: string,
bulkDomainAction: string,
registrarId: string
) {
return this.http
.post<any>(
`/console-api/bulk-domain?registrarId=${registrarId}&bulkDomainAction=${bulkDomainAction}`,
{
domainList: domainNames,
reason,
}
)
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}

updateUser(registrarId: string, updatedUser: User): Observable<any> {
return this.http
.put<User>(`/console-api/users?registrarId=${registrarId}`, updatedUser)
Expand Down
Loading

0 comments on commit d19f2b1

Please sign in to comment.