Skip to content

Commit 3911c7d

Browse files
committed
Add console bulk delete
1 parent 348cebf commit 3911c7d

File tree

12 files changed

+248
-28
lines changed

12 files changed

+248
-28
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: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,22 @@ <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+
[ngClass]="{ active: selection.hasValue() }"
71+
>
72+
<div class="console-app__domains-selection-text">
73+
{{ selection.selected.length }} Selected
74+
</div>
75+
<div class="console-app__domains-selection-actions">
76+
<button
77+
mat-flat-button
78+
aria-label="Delete Selected Domains"
79+
(click)="deleteSelectedDomains()"
80+
>
81+
Delete Selected Domains
82+
</button>
83+
</div>
7184
</div>
7285

7386
<mat-table
@@ -78,26 +91,37 @@ <h1>No domains found</h1>
7891
<!-- Checkbox Column -->
7992
<ng-container matColumnDef="select">
8093
<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()">
94+
<mat-checkbox
95+
(change)="$event ? toggleAllRows() : null"
96+
[checked]="selection.hasValue() && isAllSelected"
97+
[indeterminate]="selection.hasValue() && !isAllSelected"
98+
[aria-label]="checkboxLabel()"
99+
>
85100
</mat-checkbox>
86101
</mat-header-cell>
87102
<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)">
103+
<mat-checkbox
104+
(click)="$event.stopPropagation()"
105+
(change)="$event ? selection.toggle(row) : null"
106+
[checked]="selection.isSelected(row)"
107+
[aria-label]="checkboxLabel(row)"
108+
>
92109
</mat-checkbox>
93110
</mat-cell>
94111
</ng-container>
95112

96113
<ng-container matColumnDef="domainName">
97114
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
98-
<mat-cell *matCellDef="let element">{{
99-
element.domainName
100-
}}</mat-cell>
115+
<mat-cell *matCellDef="let element">
116+
<mat-icon
117+
*ngIf="getOperationMessage(element.domainName)"
118+
[matTooltip]="getOperationMessage(element.domainName)"
119+
matTooltipPosition="above"
120+
class="primary-text"
121+
>info</mat-icon
122+
>
123+
<span>{{ element.domainName }}</span>
124+
</mat-cell>
101125
</ng-container>
102126

103127
<ng-container matColumnDef="creationTime">

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

Lines changed: 18 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,14 @@
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+
}
5772
}
5873

5974
&__domains-spinner {

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

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,88 @@
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+
32+
interface DomainResponse {
33+
message: string;
34+
responseCode: string;
35+
}
36+
37+
interface DomainData {
38+
[domain: string]: DomainResponse;
39+
}
40+
41+
@Component({
42+
selector: 'app-response-dialog',
43+
template: `
44+
<h2 mat-dialog-title>{{ data.title }}</h2>
45+
<mat-dialog-content [innerHTML]="data.content" />
46+
<mat-dialog-actions>
47+
<button mat-button (click)="onClose()">Close</button>
48+
</mat-dialog-actions>
49+
`,
50+
})
51+
export class ResponseDialogComponent {
52+
constructor(
53+
public dialogRef: MatDialogRef<ReasonDialogComponent>,
54+
@Inject(MAT_DIALOG_DATA)
55+
public data: { title: string; content: string }
56+
) {}
57+
58+
onClose(): void {
59+
this.dialogRef.close();
60+
}
61+
}
62+
63+
@Component({
64+
selector: 'app-reason-dialog',
65+
template: `
66+
<h2 mat-dialog-title>
67+
Please provide a reason for {{ data.operation }} the domain(s):
68+
</h2>
69+
<mat-dialog-content>
70+
<mat-form-field appearance="outline" style="width:100%">
71+
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
72+
</mat-form-field>
73+
</mat-dialog-content>
74+
<mat-dialog-actions>
75+
<button mat-button (click)="onCancel()">Cancel</button>
76+
<button mat-button color="warn" (click)="onDelete()" [disabled]="!reason">
77+
Delete
78+
</button>
79+
</mat-dialog-actions>
80+
`,
81+
})
82+
export class ReasonDialogComponent {
83+
reason: string = '';
84+
85+
constructor(
86+
public dialogRef: MatDialogRef<ReasonDialogComponent>,
87+
@Inject(MAT_DIALOG_DATA)
88+
public data: { operation: 'deleting' | 'suspending' }
89+
) {}
90+
91+
onDelete(): void {
92+
this.dialogRef.close(this.reason);
93+
}
94+
95+
onCancel(): void {
96+
this.dialogRef.close();
97+
}
98+
}
2699

27100
@Component({
28101
selector: 'app-domain-list',
@@ -55,13 +128,18 @@ export class DomainListComponent {
55128
resultsPerPage = 50;
56129
totalResults?: number = 0;
57130

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

60137
constructor(
61138
protected domainListService: DomainListService,
62139
protected registrarService: RegistrarService,
63140
protected registryLockService: RegistryLockService,
64-
private _snackBar: MatSnackBar
141+
private _snackBar: MatSnackBar,
142+
private dialog: MatDialog
65143
) {
66144
effect(() => {
67145
this.pageNumber = 0;
@@ -138,6 +216,7 @@ export class DomainListComponent {
138216
onPageChange(event: PageEvent) {
139217
this.pageNumber = event.pageIndex;
140218
this.resultsPerPage = event.pageSize;
219+
this.selection.clear();
141220
this.reloadData();
142221
}
143222

@@ -156,7 +235,9 @@ export class DomainListComponent {
156235
if (!row) {
157236
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
158237
}
159-
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.domainName + 1}`;
238+
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
239+
row.domainName
240+
}`;
160241
}
161242

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

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/shared/services/backend.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,23 @@ export class BackendService {
180180
.pipe(catchError((err) => this.errorCatcher<any>(err)));
181181
}
182182

183+
bulkDomainAction(
184+
domainNames: string[],
185+
reason: string,
186+
bulkDomainAction: string,
187+
registrarId: string
188+
) {
189+
return this.http
190+
.post<any>(
191+
`/console-api/bulk-domain?registrarId=${registrarId}&bulkDomainAction=${bulkDomainAction}`,
192+
{
193+
domainList: domainNames,
194+
reason,
195+
}
196+
)
197+
.pipe(catchError((err) => this.errorCatcher<any>(err)));
198+
}
199+
183200
updateUser(registrarId: string, updatedUser: User): Observable<any> {
184201
return this.http
185202
.put<User>(`/console-api/users?registrarId=${registrarId}`, updatedUser)

console-webapp/src/theme.scss

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ $typographyConfig: mat.m2-define-typography-config(
8282
),
8383
);
8484

85-
// Access and define a class with secondary color exposed
86-
.secondary-text {
87-
color: $secondary-color;
88-
}
89-
9085
.text-xl {
9186
font-size: 18px;
9287
}
@@ -115,6 +110,15 @@ mat-row:hover {
115110
--mat-sidenav-container-width: 280px;
116111
}
117112

113+
// Access and define a class with secondary color exposed
114+
.secondary-text {
115+
color: $secondary-color;
116+
}
117+
118+
.primary-text {
119+
color: var(--primary);
120+
}
121+
118122
$theme: mat.define-theme(
119123
(
120124
color: (

0 commit comments

Comments
 (0)