From bd8e6354b58e0af398dd4bde37e3f0bfdbf72324 Mon Sep 17 00:00:00 2001
From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com>
Date: Thu, 6 Jun 2024 20:21:53 -0400
Subject: [PATCH] Add new registrar screen to the console (#2469)
---
console-webapp/src/app/app.module.ts | 2 +
.../app/registrar/newRegistrar.component.html | 182 ++++++++++++++++++
.../app/registrar/newRegistrar.component.scss | 20 ++
.../app/registrar/newRegistrar.component.ts | 99 ++++++++++
.../src/app/registrar/registrar.service.ts | 43 +++--
.../registrar/registrarDetails.component.ts | 38 ++--
.../registrar/registrarsTable.component.html | 18 +-
.../registrar/registrarsTable.component.scss | 5 +
.../registrar/registrarsTable.component.ts | 9 +-
.../app/shared/services/backend.service.ts | 8 +-
.../registry/module/RequestComponent.java | 3 +
.../frontend/FrontendRequestComponent.java | 3 +
.../console/ConsoleUpdateRegistrarAction.java | 2 +-
.../module/frontend/frontend_routing.txt | 1 +
.../google/registry/module/routing.txt | 1 +
15 files changed, 397 insertions(+), 37 deletions(-)
create mode 100644 console-webapp/src/app/registrar/newRegistrar.component.html
create mode 100644 console-webapp/src/app/registrar/newRegistrar.component.scss
create mode 100644 console-webapp/src/app/registrar/newRegistrar.component.ts
diff --git a/console-webapp/src/app/app.module.ts b/console-webapp/src/app/app.module.ts
index d14701553..5228a6c6d 100644
--- a/console-webapp/src/app/app.module.ts
+++ b/console-webapp/src/app/app.module.ts
@@ -30,6 +30,7 @@ import { DomainListComponent } from './domains/domainList.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
+import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
@@ -63,6 +64,7 @@ import { TldsComponent } from './tlds/tlds.component';
HomeComponent,
LocationBackDirective,
NavigationComponent,
+ NewRegistrarComponent,
NotificationsComponent,
RegistrarComponent,
RegistrarDetailsComponent,
diff --git a/console-webapp/src/app/registrar/newRegistrar.component.html b/console-webapp/src/app/registrar/newRegistrar.component.html
new file mode 100644
index 000000000..d7670cd91
--- /dev/null
+++ b/console-webapp/src/app/registrar/newRegistrar.component.html
@@ -0,0 +1,182 @@
+
+
+
+
Create a registrar
+
+
diff --git a/console-webapp/src/app/registrar/newRegistrar.component.scss b/console-webapp/src/app/registrar/newRegistrar.component.scss
new file mode 100644
index 000000000..7faea5410
--- /dev/null
+++ b/console-webapp/src/app/registrar/newRegistrar.component.scss
@@ -0,0 +1,20 @@
+.console-new-registrar {
+ max-width: 616px;
+
+ h2 {
+ margin: 40px 0 25px 0;
+ }
+
+ section {
+ margin-bottom: 20px;
+ }
+
+ mat-form-field {
+ display: block;
+ width: 100%;
+ }
+
+ &__submit {
+ margin: 30px 0;
+ }
+}
diff --git a/console-webapp/src/app/registrar/newRegistrar.component.ts b/console-webapp/src/app/registrar/newRegistrar.component.ts
new file mode 100644
index 000000000..c871b60ab
--- /dev/null
+++ b/console-webapp/src/app/registrar/newRegistrar.component.ts
@@ -0,0 +1,99 @@
+// Copyright 2024 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { HttpErrorResponse } from '@angular/common/http';
+import {
+ Component,
+ ElementRef,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { Registrar, RegistrarService } from './registrar.service';
+
+interface LocalizedAddressStreet {
+ line1: string;
+ line2: string;
+ line3: string;
+}
+
+@Component({
+ selector: 'app-new-registrar',
+ templateUrl: './newRegistrar.component.html',
+ styleUrls: ['./newRegistrar.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+})
+export default class NewRegistrarComponent {
+ protected newRegistrar: Registrar;
+ protected localizedAddressStreet: LocalizedAddressStreet;
+ protected billingAccountMap: String = '';
+
+ @ViewChild('form') form!: ElementRef;
+ constructor(
+ private registrarService: RegistrarService,
+ private _snackBar: MatSnackBar
+ ) {
+ this.newRegistrar = {
+ registrarId: '',
+ url: '',
+ whoisServer: '',
+ registrarName: '',
+ icannReferralEmail: '',
+ localizedAddress: {
+ city: '',
+ state: '',
+ zip: '',
+ countryCode: '',
+ },
+ };
+ this.localizedAddressStreet = {
+ line1: '',
+ line2: '',
+ line3: '',
+ };
+ }
+
+ onBillingAccountMapChange(val: String) {
+ const billingAccountMap: { [key: string]: string } = {};
+ this.newRegistrar.billingAccountMap = val.split('\n').reduce((acc, val) => {
+ const [currency, billingCode] = val.split('=');
+ acc[currency] = billingCode;
+ return acc;
+ }, billingAccountMap);
+ }
+
+ save(e: SubmitEvent) {
+ e.preventDefault();
+ if (this.form.nativeElement.checkValidity()) {
+ const { line1, line2, line3 } = this.localizedAddressStreet;
+ this.newRegistrar.localizedAddress.street = [line1, line2, line3].filter(
+ (v) => !!v
+ );
+ this.registrarService.createRegistrar(this.newRegistrar).subscribe({
+ complete: () => {
+ this.goBack();
+ },
+ error: (err: HttpErrorResponse) => {
+ this._snackBar.open(err.error);
+ },
+ });
+ } else {
+ this.form.nativeElement.reportValidity();
+ }
+ }
+
+ goBack() {
+ this.registrarService.inNewRegistrarMode.set(false);
+ }
+}
diff --git a/console-webapp/src/app/registrar/registrar.service.ts b/console-webapp/src/app/registrar/registrar.service.ts
index 35f45af71..87b20e99d 100644
--- a/console-webapp/src/app/registrar/registrar.service.ts
+++ b/console-webapp/src/app/registrar/registrar.service.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable, computed, signal } from '@angular/core';
-import { Observable, tap } from 'rxjs';
+import { Observable, tap, switchMap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
@@ -85,14 +85,21 @@ export class RegistrarService implements GlobalLoader {
this.registrars().find((r) => r.registrarId === this.registrarId())
);
+ inNewRegistrarMode = signal(false);
+
+ registrarsLoaded: Promise;
+
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar,
private router: Router
) {
- this.loadRegistrars().subscribe((r) => {
- this.globalLoader.stopGlobalLoader(this);
+ this.registrarsLoaded = new Promise((resolve) => {
+ this.loadRegistrars().subscribe((r) => {
+ this.globalLoader.stopGlobalLoader(this);
+ resolve();
+ });
});
this.globalLoader.startGlobalLoader(this);
}
@@ -118,19 +125,23 @@ export class RegistrarService implements GlobalLoader {
);
}
- saveRegistrar(registrar: Registrar) {
- return this.backend.postRegistrar(registrar).pipe(
- tap((registrar) => {
- if (registrar) {
- this.registrars.set(
- this.registrars().map((r) => {
- if (r.registrarId === registrar.registrarId) {
- return registrar;
- }
- return r;
- })
- );
- }
+ createRegistrar(registrar: Registrar) {
+ return this.backend
+ .createRegistrar(registrar)
+ .pipe(switchMap((_) => this.loadRegistrars()));
+ }
+
+ updateRegistrar(updatedRegistrar: Registrar) {
+ return this.backend.updateRegistrar(updatedRegistrar).pipe(
+ tap(() => {
+ this.registrars.set(
+ this.registrars().map((r) => {
+ if (r.registrarId === updatedRegistrar.registrarId) {
+ return updatedRegistrar;
+ }
+ return r;
+ })
+ );
})
);
}
diff --git a/console-webapp/src/app/registrar/registrarDetails.component.ts b/console-webapp/src/app/registrar/registrarDetails.component.ts
index d306b31bd..74963fe03 100644
--- a/console-webapp/src/app/registrar/registrarDetails.component.ts
+++ b/console-webapp/src/app/registrar/registrarDetails.component.ts
@@ -42,28 +42,32 @@ export class RegistrarDetailsComponent implements OnInit {
) {}
ngOnInit(): void {
- this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
- this.registrarInEdit = structuredClone(
- this.registrarService
- .registrars()
- .filter((r) => r.registrarId === params.get('id'))[0]
- );
- if (!this.registrarInEdit) {
- this._snackBar.open(
- `Registrar with id ${params.get('id')} is not available`
+ this.registrarService.registrarsLoaded.then(() => {
+ this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
+ this.registrarInEdit = structuredClone(
+ this.registrarService
+ .registrars()
+ .filter((r) => r.registrarId === params.get('id'))[0]
);
- this.registrarNotFound = true;
- } else {
- this.registrarNotFound = false;
- }
+ if (!this.registrarInEdit) {
+ this._snackBar.open(
+ `Registrar with id ${params.get('id')} is not available`
+ );
+ this.registrarNotFound = true;
+ } else {
+ this.registrarNotFound = false;
+ }
+ });
});
}
addTLD(e: MatChipInputEvent) {
+ this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds || [];
this.removeTLD(e.value); // Prevent dups
- this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
- [e.value.toLowerCase()]
- );
+ this.registrarInEdit.allowedTlds = [
+ ...this.registrarInEdit.allowedTlds,
+ e.value.toLowerCase(),
+ ];
}
removeTLD(tld: string) {
@@ -73,7 +77,7 @@ export class RegistrarDetailsComponent implements OnInit {
}
saveAndClose() {
- this.registrarService.saveRegistrar(this.registrarInEdit).subscribe({
+ this.registrarService.updateRegistrar(this.registrarInEdit).subscribe({
complete: () => {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
diff --git a/console-webapp/src/app/registrar/registrarsTable.component.html b/console-webapp/src/app/registrar/registrarsTable.component.html
index 8922f06f5..5484322f7 100644
--- a/console-webapp/src/app/registrar/registrarsTable.component.html
+++ b/console-webapp/src/app/registrar/registrarsTable.component.html
@@ -1,5 +1,19 @@
+@if(registrarService.inNewRegistrarMode()) {
+
+} @else {
-
Registrars
+
Search
+
+}
diff --git a/console-webapp/src/app/registrar/registrarsTable.component.scss b/console-webapp/src/app/registrar/registrarsTable.component.scss
index fb482ab01..896b4c34e 100644
--- a/console-webapp/src/app/registrar/registrarsTable.component.scss
+++ b/console-webapp/src/app/registrar/registrarsTable.component.scss
@@ -15,6 +15,11 @@
min-width: $min-width !important;
}
+ &__registrars-header {
+ display: flex;
+ justify-content: space-between;
+ }
+
.mat-mdc-paginator {
min-width: $min-width !important;
}
diff --git a/console-webapp/src/app/registrar/registrarsTable.component.ts b/console-webapp/src/app/registrar/registrarsTable.component.ts
index c0bdfe363..3bbe64dcd 100644
--- a/console-webapp/src/app/registrar/registrarsTable.component.ts
+++ b/console-webapp/src/app/registrar/registrarsTable.component.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
+import { Component, effect, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
@@ -93,6 +93,9 @@ export class RegistrarComponent {
this.dataSource = new MatTableDataSource(
registrarService.registrars()
);
+ effect(() => {
+ this.dataSource.data = registrarService.registrars();
+ });
}
ngAfterViewInit() {
@@ -111,4 +114,8 @@ export class RegistrarComponent {
// TODO: consider filteing out only by registrar name
this.dataSource.filter = filterValue.trim().toLowerCase();
}
+
+ openNewRegistrar() {
+ this.registrarService.inNewRegistrarMode.set(true);
+ }
}
diff --git a/console-webapp/src/app/shared/services/backend.service.ts b/console-webapp/src/app/shared/services/backend.service.ts
index b8a00137c..698cbbb04 100644
--- a/console-webapp/src/app/shared/services/backend.service.ts
+++ b/console-webapp/src/app/shared/services/backend.service.ts
@@ -110,7 +110,13 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher(err)));
}
- postRegistrar(registrar: Registrar): Observable {
+ createRegistrar(registrar: Registrar): Observable {
+ return this.http
+ .post('/console-api/registrars', registrar)
+ .pipe(catchError((err) => this.errorCatcher(err)));
+ }
+
+ updateRegistrar(registrar: Registrar): Observable {
return this.http
.post('/console-api/registrar', registrar)
.pipe(catchError((err) => this.errorCatcher(err)));
diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java
index 1461b3892..7f74c8768 100644
--- a/core/src/main/java/google/registry/module/RequestComponent.java
+++ b/core/src/main/java/google/registry/module/RequestComponent.java
@@ -114,6 +114,7 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
+import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -192,6 +193,8 @@ interface RequestComponent {
ConsoleUiAction consoleUiAction();
+ ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
+
ConsoleUserDataAction consoleUserDataAction();
ConsoleDumDownloadAction consoleDumDownloadAction();
diff --git a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
index 5212b40aa..15a403e76 100644
--- a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
+++ b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
@@ -30,6 +30,7 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
+import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -70,6 +71,8 @@ public interface FrontendRequestComponent {
ConsoleUiAction consoleUiAction();
+ ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
+
ConsoleUserDataAction consoleUserDataAction();
ConsoleDumDownloadAction consoleDumDownloadAction();
diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java
index 95de960c9..b94b6b374 100644
--- a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java
+++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java
@@ -38,7 +38,7 @@ import javax.inject.Inject;
@Action(
service = Action.Service.DEFAULT,
- path = ConsoleEppPasswordAction.PATH,
+ path = ConsoleUpdateRegistrarAction.PATH,
method = {POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
diff --git a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt
index 0033f8154..0d6fabd8f 100644
--- a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt
+++ b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt
@@ -4,6 +4,7 @@ PATH CLASS METHODS OK MIN
/console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
/console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
+/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt
index 6f08df2b3..a59e95878 100644
--- a/core/src/test/resources/google/registry/module/routing.txt
+++ b/core/src/test/resources/google/registry/module/routing.txt
@@ -60,6 +60,7 @@ PATH CLASS
/console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
/console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
+/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC