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

+
+

General

+
+ + Registrar Name: + + +
+
+ + Registrar ID: + + +
+
+ + Registrar email address: + + +
+
+ + Billing Accounts: + + +
+
+ + IANA ID: + + +
+
+ + ICANN referral email: + + +
+
+ + Drive ID: + + +
+

Contact Info

+
+ + Street address (Line 1): + + +
+
+ + Street address (Line 2) + + +
+
+ + Street address (Line 3) + + +
+
+ + City: + + +
+
+ + State/Region: + + +
+
+ + ZIP/Postal Code: + + +
+
+ + Country Code (e.g. US): + + +
+ +
+
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

+
+

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