mirror of
https://github.com/google/nomulus
synced 2026-01-06 21:47:31 +00:00
Add new registrar screen to the console (#2469)
This commit is contained in:
@@ -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,
|
||||
|
||||
182
console-webapp/src/app/registrar/newRegistrar.component.html
Normal file
182
console-webapp/src/app/registrar/newRegistrar.component.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<div class="console-new-registrar">
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to registrars list"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<h1>Create a registrar</h1>
|
||||
<form (ngSubmit)="save($event)" #form>
|
||||
<h2>General</h2>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar Name: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.registrarName"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar ID: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.registrarId"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar email address: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Billing Accounts: </mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
required="true"
|
||||
placeholder="USD=billing-id-for-usd
|
||||
JPY=billing-id-for-yen"
|
||||
[ngModel]="billingAccountMap"
|
||||
(ngModelChange)="onBillingAccountMapChange($event)"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>IANA ID: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.ianaIdentifier"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>ICANN referral email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
type="email"
|
||||
[(ngModel)]="newRegistrar.icannReferralEmail"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Drive ID: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.driveFolderId"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<h2>Contact Info</h2>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street address (Line 1): </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="localizedAddressStreet.line1"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street address (Line 2)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="false"
|
||||
[(ngModel)]="localizedAddressStreet.line2"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street address (Line 3)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="false"
|
||||
[(ngModel)]="localizedAddressStreet.line3"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>City: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.city"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>State/Region: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="false"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.state"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>ZIP/Postal Code: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.zip"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Country Code (e.g. US): </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.countryCode"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<button
|
||||
class="console-new-registrar__submit"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
20
console-webapp/src/app/registrar/newRegistrar.component.scss
Normal file
20
console-webapp/src/app/registrar/newRegistrar.component.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
99
console-webapp/src/app/registrar/newRegistrar.component.ts
Normal file
99
console-webapp/src/app/registrar/newRegistrar.component.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<void>;
|
||||
|
||||
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;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
@if(registrarService.inNewRegistrarMode()) {
|
||||
<app-new-registrar />
|
||||
} @else {
|
||||
<div class="console-app__registrars">
|
||||
<h1 class="mat-headline-4">Registrars</h1>
|
||||
<div class="console-app__registrars-header">
|
||||
<h1 class="mat-headline-4">Registrars</h1>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="openNewRegistrar()"
|
||||
aria-label="Add new registrar"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
Add new registrar
|
||||
</button>
|
||||
</div>
|
||||
<mat-form-field class="console-app__registrars-filter">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
@@ -36,3 +50,5 @@
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Registrar>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,13 @@ export class BackendService {
|
||||
.pipe(catchError((err) => this.errorCatcher<Registrar[]>(err)));
|
||||
}
|
||||
|
||||
postRegistrar(registrar: Registrar): Observable<Registrar> {
|
||||
createRegistrar(registrar: Registrar): Observable<Registrar> {
|
||||
return this.http
|
||||
.post<Registrar>('/console-api/registrars', registrar)
|
||||
.pipe(catchError((err) => this.errorCatcher<Registrar>(err)));
|
||||
}
|
||||
|
||||
updateRegistrar(registrar: Registrar): Observable<Registrar> {
|
||||
return this.http
|
||||
.post<Registrar>('/console-api/registrar', registrar)
|
||||
.pipe(catchError((err) => this.errorCatcher<Registrar>(err)));
|
||||
|
||||
Reference in New Issue
Block a user