Skip to content

Commit 2ad081f

Browse files
committed
Add console bulk delete
1 parent a76ac56 commit 2ad081f

19 files changed

+284
-40
lines changed

console-webapp/src/app/app.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import { BackendService } from './shared/services/backend.service';
2626
import { provideHttpClient } from '@angular/common/http';
2727
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
2828
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
29-
import { DomainListComponent } from './domains/domainList.component';
29+
import {
30+
DomainListComponent,
31+
ReasonDialogComponent,
32+
ResponseDialogComponent,
33+
} from './domains/domainList.component';
3034
import { RegistryLockComponent } from './domains/registryLock.component';
3135
import { HeaderComponent } from './header/header.component';
3236
import { HomeComponent } from './home/home.component';
@@ -92,6 +96,8 @@ export class SelectedRegistrarModule {}
9296
TldsComponent,
9397
WhoisComponent,
9498
WhoisEditComponent,
99+
ReasonDialogComponent,
100+
ResponseDialogComponent,
95101
],
96102
bootstrap: [AppComponent],
97103
imports: [

console-webapp/src/app/domains/domainList.component.html

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,23 @@ <h1>No domains found</h1>
6565
/>
6666
</mat-form-field>
6767

68-
<div class="console-app__domains-selection" [ngClass]="{'active': selection.hasValue()}">
69-
<div class="console-app__domains-selection-text">{{selection.selected.length}} Selected</div>
70-
<div class="console-app__domains-selection-actions"></div>
68+
<div
69+
class="console-app__domains-selection"
70+
[elementId]="getElementIdForBulkDelete()"
71+
[ngClass]="{ active: selection.hasValue() }"
72+
>
73+
<div class="console-app__domains-selection-text">
74+
{{ selection.selected.length }} Selected
75+
</div>
76+
<div class="console-app__domains-selection-actions">
77+
<button
78+
mat-flat-button
79+
aria-label="Delete Selected Domains"
80+
(click)="deleteSelectedDomains()"
81+
>
82+
Delete Selected Domains
83+
</button>
84+
</div>
7185
</div>
7286

7387
<mat-table
@@ -78,26 +92,39 @@ <h1>No domains found</h1>
7892
<!-- Checkbox Column -->
7993
<ng-container matColumnDef="select">
8094
<mat-header-cell *matHeaderCellDef>
81-
<mat-checkbox (change)="$event ? toggleAllRows() : null"
82-
[checked]="selection.hasValue() && isAllSelected"
83-
[indeterminate]="selection.hasValue() && !isAllSelected"
84-
[aria-label]="checkboxLabel()">
95+
<mat-checkbox
96+
(change)="$event ? toggleAllRows() : null"
97+
[checked]="selection.hasValue() && isAllSelected"
98+
[indeterminate]="selection.hasValue() && !isAllSelected"
99+
[aria-label]="checkboxLabel()"
100+
[elementId]="getElementIdForBulkDelete()"
101+
>
85102
</mat-checkbox>
86103
</mat-header-cell>
87104
<mat-cell *matCellDef="let row">
88-
<mat-checkbox (click)="$event.stopPropagation()"
89-
(change)="$event ? selection.toggle(row) : null"
90-
[checked]="selection.isSelected(row)"
91-
[aria-label]="checkboxLabel(row)">
105+
<mat-checkbox
106+
(click)="$event.stopPropagation()"
107+
(change)="$event ? selection.toggle(row) : null"
108+
[checked]="selection.isSelected(row)"
109+
[aria-label]="checkboxLabel(row)"
110+
[elementId]="getElementIdForBulkDelete()"
111+
>
92112
</mat-checkbox>
93113
</mat-cell>
94114
</ng-container>
95115

96116
<ng-container matColumnDef="domainName">
97117
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
98-
<mat-cell *matCellDef="let element">{{
99-
element.domainName
100-
}}</mat-cell>
118+
<mat-cell *matCellDef="let element">
119+
<mat-icon
120+
*ngIf="getOperationMessage(element.domainName)"
121+
[matTooltip]="getOperationMessage(element.domainName)"
122+
matTooltipPosition="above"
123+
class="primary-text"
124+
>info</mat-icon
125+
>
126+
<span>{{ element.domainName }}</span>
127+
</mat-cell>
101128
</ng-container>
102129

103130
<ng-container matColumnDef="creationTime">

console-webapp/src/app/domains/domainList.component.scss

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@
1313
}
1414

1515
&__domains-selection {
16-
height: 30px;
16+
height: 60px;
1717
max-height: 0;
18-
transition: max-height .2s linear;
18+
transition: max-height 0.2s linear;
19+
display: flex;
20+
align-items: center;
21+
overflow: hidden;
22+
gap: 20px;
23+
&-text {
24+
font-weight: bold;
25+
}
1926
&.active {
20-
max-height: 30px;
27+
max-height: 60px;
2128
}
2229
}
2330

@@ -54,6 +61,18 @@
5461
max-width: 60px;
5562
padding-left: 15px;
5663
}
64+
.mat-column-domainName {
65+
position: relative;
66+
padding-left: 25px;
67+
mat-icon {
68+
position: absolute;
69+
left: 0;
70+
}
71+
}
72+
mat-cell:has([style*="display: none"]),
73+
mat-header-cell:has([style*="display: none"]) {
74+
display: none;
75+
}
5776
}
5877

5978
&__domains-spinner {

console-webapp/src/app/domains/domainList.component.ts

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,89 @@
1414

1515
import { SelectionModel } from '@angular/cdk/collections';
1616
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
17-
import { Component, ViewChild, effect } from '@angular/core';
17+
import { Component, ViewChild, effect, Inject } from '@angular/core';
1818
import { MatPaginator, PageEvent } from '@angular/material/paginator';
1919
import { MatSnackBar } from '@angular/material/snack-bar';
2020
import { MatTableDataSource } from '@angular/material/table';
21-
import { Subject, debounceTime } from 'rxjs';
21+
import { Subject, debounceTime, take, filter } from 'rxjs';
2222
import { RegistrarService } from '../registrar/registrar.service';
2323
import { Domain, DomainListService } from './domainList.service';
2424
import { RegistryLockComponent } from './registryLock.component';
2525
import { RegistryLockService } from './registryLock.service';
26+
import {
27+
MAT_DIALOG_DATA,
28+
MatDialog,
29+
MatDialogRef,
30+
} from '@angular/material/dialog';
31+
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
32+
33+
interface DomainResponse {
34+
message: string;
35+
responseCode: string;
36+
}
37+
38+
interface DomainData {
39+
[domain: string]: DomainResponse;
40+
}
41+
42+
@Component({
43+
selector: 'app-response-dialog',
44+
template: `
45+
<h2 mat-dialog-title>{{ data.title }}</h2>
46+
<mat-dialog-content [innerHTML]="data.content" />
47+
<mat-dialog-actions>
48+
<button mat-button (click)="onClose()">Close</button>
49+
</mat-dialog-actions>
50+
`,
51+
})
52+
export class ResponseDialogComponent {
53+
constructor(
54+
public dialogRef: MatDialogRef<ReasonDialogComponent>,
55+
@Inject(MAT_DIALOG_DATA)
56+
public data: { title: string; content: string }
57+
) {}
58+
59+
onClose(): void {
60+
this.dialogRef.close();
61+
}
62+
}
63+
64+
@Component({
65+
selector: 'app-reason-dialog',
66+
template: `
67+
<h2 mat-dialog-title>
68+
Please provide a reason for {{ data.operation }} the domain(s):
69+
</h2>
70+
<mat-dialog-content>
71+
<mat-form-field appearance="outline" style="width:100%">
72+
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
73+
</mat-form-field>
74+
</mat-dialog-content>
75+
<mat-dialog-actions>
76+
<button mat-button (click)="onCancel()">Cancel</button>
77+
<button mat-button color="warn" (click)="onDelete()" [disabled]="!reason">
78+
Delete
79+
</button>
80+
</mat-dialog-actions>
81+
`,
82+
})
83+
export class ReasonDialogComponent {
84+
reason: string = '';
85+
86+
constructor(
87+
public dialogRef: MatDialogRef<ReasonDialogComponent>,
88+
@Inject(MAT_DIALOG_DATA)
89+
public data: { operation: 'deleting' | 'suspending' }
90+
) {}
91+
92+
onDelete(): void {
93+
this.dialogRef.close(this.reason);
94+
}
95+
96+
onCancel(): void {
97+
this.dialogRef.close();
98+
}
99+
}
26100

27101
@Component({
28102
selector: 'app-domain-list',
@@ -55,13 +129,18 @@ export class DomainListComponent {
55129
resultsPerPage = 50;
56130
totalResults?: number = 0;
57131

132+
reason: string = '';
133+
134+
operationResult: DomainData | undefined;
135+
58136
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
59137

60138
constructor(
61139
protected domainListService: DomainListService,
62140
protected registrarService: RegistrarService,
63141
protected registryLockService: RegistryLockService,
64-
private _snackBar: MatSnackBar
142+
private _snackBar: MatSnackBar,
143+
private dialog: MatDialog
65144
) {
66145
effect(() => {
67146
this.pageNumber = 0;
@@ -138,6 +217,7 @@ export class DomainListComponent {
138217
onPageChange(event: PageEvent) {
139218
this.pageNumber = event.pageIndex;
140219
this.resultsPerPage = event.pageSize;
220+
this.selection.clear();
141221
this.reloadData();
142222
}
143223

@@ -156,7 +236,9 @@ export class DomainListComponent {
156236
if (!row) {
157237
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
158238
}
159-
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.domainName + 1}`;
239+
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
240+
row.domainName
241+
}`;
160242
}
161243

162244
private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
@@ -168,4 +250,65 @@ export class DomainListComponent {
168250
return this.isAllSelected || o1.domainName === o2.domainName;
169251
};
170252
}
253+
254+
getElementIdForBulkDelete() {
255+
return RESTRICTED_ELEMENTS.BULK_DELETE;
256+
}
257+
258+
getOperationMessage(domain: string) {
259+
if (this.operationResult && this.operationResult[domain])
260+
return this.operationResult[domain].message;
261+
return '';
262+
}
263+
264+
sendDeleteRequest(reason: string) {
265+
this.isLoading = true;
266+
this.domainListService
267+
.deleteDomains(
268+
this.selection.selected,
269+
reason,
270+
this.registrarService.registrarId()
271+
)
272+
.pipe(take(1))
273+
.subscribe({
274+
next: (result: DomainData) => {
275+
this.isLoading = false;
276+
const successCount = Object.keys(result).filter((domainName) =>
277+
result[domainName].responseCode.toString().startsWith('1')
278+
).length;
279+
const failureCount = Object.keys(result).length - successCount;
280+
this.dialog.open(ResponseDialogComponent, {
281+
data: {
282+
title: 'Domain Deletion Results',
283+
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
284+
failureCount
285+
? 'Some domains could not be deleted due to ongoing processes or server errors. '
286+
: ''
287+
}Please check the table for more information.`,
288+
},
289+
});
290+
this.selection.clear();
291+
this.operationResult = result;
292+
this.reloadData();
293+
},
294+
error: (err: HttpErrorResponse) =>
295+
this._snackBar.open(err.error || err.message),
296+
});
297+
}
298+
299+
deleteSelectedDomains() {
300+
const dialogRef = this.dialog.open(ReasonDialogComponent, {
301+
data: {
302+
operation: 'deleting',
303+
},
304+
});
305+
306+
dialogRef
307+
.afterClosed()
308+
.pipe(
309+
take(1),
310+
filter((reason) => !!reason)
311+
)
312+
.subscribe(this.sendDeleteRequest.bind(this));
313+
}
171314
}

console-webapp/src/app/domains/domainList.service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export class DomainListService {
4848
private backendService: BackendService,
4949
private registrarService: RegistrarService
5050
) {}
51-
5251
retrieveDomains(
5352
pageNumber?: number,
5453
resultsPerPage?: number,
@@ -71,4 +70,13 @@ export class DomainListService {
7170
})
7271
);
7372
}
73+
74+
deleteDomains(domains: Domain[], reason: string, registrarId: string) {
75+
return this.backendService.bulkDomainAction(
76+
domains.map((d) => d.domainName),
77+
reason,
78+
'DELETE',
79+
registrarId
80+
);
81+
}
7482
}

console-webapp/src/app/registrar/registrarSelector.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
[ngModelOptions]="{ standalone: true }"
1212
(focus)="onFocus()"
1313
[matAutocomplete]="auto"
14+
spellcheck="false"
1415
/>
1516
<mat-autocomplete
1617
autoActiveFirstOption

console-webapp/src/app/shared/directives/userLevelVisiblity.directive.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ export enum RESTRICTED_ELEMENTS {
1919
REGISTRAR_ELEMENT,
2020
OTE,
2121
USERS,
22+
BULK_DELETE,
2223
}
2324

2425
export const DISABLED_ELEMENTS_PER_ROLE = {
2526
NONE: [
2627
RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT,
2728
RESTRICTED_ELEMENTS.OTE,
2829
RESTRICTED_ELEMENTS.USERS,
30+
RESTRICTED_ELEMENTS.BULK_DELETE,
2931
],
3032
SUPPORT_LEAD: [RESTRICTED_ELEMENTS.USERS],
3133
SUPPORT_AGENT: [RESTRICTED_ELEMENTS.USERS],

0 commit comments

Comments
 (0)