Add registrar selection functionality (#2054)

This commit is contained in:
Pavlo Tkach 2023-06-14 16:51:54 -04:00 committed by GitHub
parent 952a92a5db
commit 86b62ebe76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 350 additions and 65 deletions

View file

@ -1,7 +1,7 @@
{ {
"/registrar": "/console-api":
{ {
"target": "http://localhost:8080/registrar", "target": "http://localhost:8080",
"secure": false "secure": true
} }
} }

View file

@ -22,14 +22,18 @@ import SettingsRegistrarsComponent from './settings/registrars/registrars.compon
import SettingsWhoisComponent from './settings/whois/whois.component'; import SettingsWhoisComponent from './settings/whois/whois.component';
import SettingsUsersComponent from './settings/users/users.component'; import SettingsUsersComponent from './settings/users/users.component';
import SettingsSecurityComponent from './settings/security/security.component'; import SettingsSecurityComponent from './settings/security/security.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import { RegistrarComponent } from './registrar/registrar.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', redirectTo: '/settings/contact', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent }, { path: 'registrars', component: RegistrarComponent },
{ path: 'tlds', component: TldsComponent }, { path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{ {
path: 'settings', path: 'settings',
component: SettingsComponent, component: SettingsComponent,
canActivate: [RegistrarGuard],
children: [ children: [
{ {
path: '', path: '',

View file

@ -12,6 +12,9 @@
<a mat-list-item [routerLink]="'/settings'" routerLinkActive="active"> <a mat-list-item [routerLink]="'/settings'" routerLinkActive="active">
Settings Settings
</a> </a>
<a mat-list-item [routerLink]="'/registrars'" routerLinkActive="active">
Select Registrar
</a>
</mat-nav-list> </mat-nav-list>
</mat-sidenav> </mat-sidenav>
<mat-sidenav-content class="console-app__content"> <mat-sidenav-content class="console-app__content">

View file

@ -31,6 +31,8 @@ import SettingsContactComponent, {
ContactDetailsDialogComponent, ContactDetailsDialogComponent,
} from './settings/contact/contact.component'; } from './settings/contact/contact.component';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { RegistrarComponent } from './registrar/registrar.component';
import { RegistrarGuard } from './registrar/registrar.guard';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -41,6 +43,7 @@ import { HttpClientModule } from '@angular/common/http';
SettingsComponent, SettingsComponent,
SettingsContactComponent, SettingsContactComponent,
ContactDetailsDialogComponent, ContactDetailsDialogComponent,
RegistrarComponent,
], ],
imports: [ imports: [
HttpClientModule, HttpClientModule,
@ -50,7 +53,7 @@ import { HttpClientModule } from '@angular/common/http';
AppRoutingModule, AppRoutingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
], ],
providers: [BackendService], providers: [BackendService, RegistrarGuard],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View file

@ -0,0 +1,17 @@
<div class="console-app__registrar">
<h4 class="console-app__title">
{{ registrarService.activeRegistrarId === "" ? "Select" : "Switch" }}
registrar:
</h4>
<mat-form-field>
<mat-label>Registrar</mat-label>
<mat-select [(ngModel)]="registrarService.activeRegistrarId">
<mat-option
*ngFor="let registrar of registrarService.registrars"
[value]="registrar"
>
{{ registrar }}
</mat-option>
</mat-select>
</mat-form-field>
</div>

View file

@ -0,0 +1,11 @@
.console-app {
&__registrar {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
}
&__title {
margin-right: 1rem;
}
}

View file

@ -0,0 +1,44 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarComponent } from './registrar.component';
import { BackendService } from '../shared/services/backend.service';
import { ActivatedRoute } from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('RegistrarComponent', () => {
let component: RegistrarComponent;
let fixture: ComponentFixture<RegistrarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarComponent],
imports: [HttpClientTestingModule],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
],
}).compileComponents();
fixture = TestBed.createComponent(RegistrarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,45 @@
// Copyright 2023 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 { Component } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-registrar',
templateUrl: './registrar.component.html',
styleUrls: ['./registrar.component.less'],
})
export class RegistrarComponent {
private lastActiveRegistrarId: string;
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private router: Router
) {
this.lastActiveRegistrarId = registrarService.activeRegistrarId;
}
ngDoCheck() {
if (
this.registrarService.activeRegistrarId &&
this.registrarService.activeRegistrarId !== this.lastActiveRegistrarId &&
this.route.snapshot.paramMap.get('nextUrl')
) {
this.lastActiveRegistrarId = this.registrarService.activeRegistrarId;
this.router.navigate([this.route.snapshot.paramMap.get('nextUrl')]);
}
}
}

View file

@ -0,0 +1,64 @@
// Copyright 2023 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 { TestBed } from '@angular/core/testing';
import { RegistrarGuard } from './registrar.guard';
import { Router, RouterStateSnapshot } from '@angular/router';
import { RegistrarService } from './registrar.service';
describe('RegistrarGuard', () => {
let guard: RegistrarGuard;
let dummyRegistrarService: RegistrarService;
let routeSpy: Router;
let dummyRoute: RouterStateSnapshot = {} as RouterStateSnapshot;
beforeEach(() => {
routeSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
dummyRegistrarService = { activeRegistrarId: '' } as RegistrarService;
TestBed.configureTestingModule({
providers: [
RegistrarGuard,
{ provide: Router, useValue: routeSpy },
{ provide: RegistrarService, useValue: dummyRegistrarService },
],
});
});
it('should not be able to activate when activeRegistrarId is empty', () => {
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate(dummyRoute);
expect(res).toBeFalsy();
});
it('should be able to activate when activeRegistrarId is not empty', () => {
TestBed.overrideProvider(RegistrarService, {
useValue: { activeRegistrarId: 'value' },
});
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate(dummyRoute);
expect(res).toBeTrue();
});
it('should navigate to registrars when activeRegistrarId is empty', () => {
const dummyRoute = { url: '/value' } as RouterStateSnapshot;
guard = TestBed.inject(RegistrarGuard);
guard.canActivate(dummyRoute);
expect(routeSpy.navigate).toHaveBeenCalledOnceWith([
'/registrars',
{ nextUrl: '/value' },
]);
});
});

View file

@ -0,0 +1,34 @@
// Copyright 2023 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 { Injectable } from '@angular/core';
import { Router, RouterStateSnapshot } from '@angular/router';
import { RegistrarService } from './registrar.service';
@Injectable({
providedIn: 'root',
})
export class RegistrarGuard {
constructor(
private router: Router,
private registrarService: RegistrarService
) {}
canActivate(state: RouterStateSnapshot): Promise<boolean> | boolean {
if (this.registrarService.activeRegistrarId) {
return true;
}
return this.router.navigate([`/registrars`, { nextUrl: state.url }]);
}
}

View file

@ -0,0 +1,35 @@
// Copyright 2023 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 { TestBed } from '@angular/core/testing';
import { RegistrarService } from './registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('RegistrarService', () => {
let service: RegistrarService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [BackendService],
});
service = TestBed.inject(RegistrarService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,29 @@
// Copyright 2023 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 { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
@Injectable({
providedIn: 'root',
})
export class RegistrarService {
activeRegistrarId: string = '';
registrars: string[] = [];
constructor(private backend: BackendService) {
this.backend.getRegistrars().subscribe((r) => {
this.registrars = r;
});
}
}

View file

@ -1,11 +1,12 @@
<h3 mat-dialog-title>Contact details</h3> <h3 mat-dialog-title>Contact details</h3>
<div mat-dialog-content> <div mat-dialog-content>
<form> <form (ngSubmit)="saveAndClose($event)">
<div> <div>
<mat-form-field class="contact-details__input"> <mat-form-field class="contact-details__input">
<mat-label>Name: </mat-label> <mat-label>Name: </mat-label>
<input <input
matInput matInput
[required]="true"
[(ngModel)]="contact.name" [(ngModel)]="contact.name"
[ngModelOptions]="{ standalone: true }" [ngModelOptions]="{ standalone: true }"
/> />
@ -19,6 +20,7 @@
type="email" type="email"
matInput matInput
[email]="true" [email]="true"
[required]="true"
[(ngModel)]="contact.emailAddress" [(ngModel)]="contact.emailAddress"
[ngModelOptions]="{ standalone: true }" [ngModelOptions]="{ standalone: true }"
/> />
@ -94,12 +96,9 @@
(per CL&D requirements)</mat-checkbox (per CL&D requirements)</mat-checkbox
> >
</section> </section>
</form>
<p></p>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button (click)="onClose()">Cancel</button> <button mat-button (click)="onClose()">Cancel</button>
<button mat-button (click)="saveAndClose()">Save</button> <button type="submit" mat-button>Save</button>
</mat-dialog-actions> </mat-dialog-actions>
</form>
</div> </div>

View file

@ -14,7 +14,10 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContactComponent } from './contact.component'; import ContactComponent from './contact.component';
import { MaterialModule } from 'src/app/material.module';
import { BackendService } from 'src/app/shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('ContactComponent', () => { describe('ContactComponent', () => {
let component: ContactComponent; let component: ContactComponent;
@ -23,8 +26,9 @@ describe('ContactComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ContactComponent], declarations: [ContactComponent],
imports: [HttpClientTestingModule, MaterialModule],
providers: [BackendService],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(ContactComponent); fixture = TestBed.createComponent(ContactComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

View file

@ -104,7 +104,11 @@ export class ContactDetailsDialogComponent {
this.operation = data.operation; this.operation = data.operation;
} }
saveAndClose() { saveAndClose(e: any) {
e.preventDefault();
if (!e.target.checkValidity()) {
return;
}
let operationObservable; let operationObservable;
if (this.operation === Operations.ADD) { if (this.operation === Operations.ADD) {
operationObservable = this.contactService.addContact(this.contact); operationObservable = this.contactService.addContact(this.contact);
@ -143,7 +147,7 @@ export default class ContactComponent {
) { ) {
// TODO: Refactor to registrarId service // TODO: Refactor to registrarId service
this.loading = true; this.loading = true;
this.contactService.fetchContacts('zoomco').subscribe(() => { this.contactService.fetchContacts().subscribe(() => {
this.loading = false; this.loading = false;
}); });
} }
@ -160,7 +164,9 @@ export default class ContactComponent {
} }
deleteContact(contact: Contact) { deleteContact(contact: Contact) {
this.contactService.deleteContact(contact); if (confirm(`Please confirm contact ${contact.name} delete`)) {
this.contactService.deleteContact(contact).subscribe();
}
} }
openCreateNew(e: Event) { openCreateNew(e: Event) {

View file

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs'; import { Observable, tap } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service'; import { BackendService } from 'src/app/shared/services/backend.service';
export interface Contact { export interface Contact {
@ -34,22 +35,26 @@ export interface Contact {
export class ContactService { export class ContactService {
contacts: Contact[] = []; contacts: Contact[] = [];
constructor(private backend: BackendService) {} constructor(
private backend: BackendService,
private registrarService: RegistrarService
) {}
// TODO: Come up with a better handling for registrarId // TODO: Come up with a better handling for registrarId
fetchContacts(registrarId: string): Observable<Contact[]> { fetchContacts(): Observable<Contact[]> {
return this.backend.getContacts(registrarId).pipe( return this.backend
.getContacts(this.registrarService.activeRegistrarId)
.pipe(
tap((contacts) => { tap((contacts) => {
this.contacts = contacts; this.contacts = contacts;
}) })
); );
} }
saveContacts( saveContacts(contacts: Contact[]): Observable<Contact[]> {
contacts: Contact[], return this.backend
registrarId?: string .postContacts(this.registrarService.activeRegistrarId, contacts)
): Observable<Contact[]> { .pipe(
return this.backend.postContacts(registrarId || 'default', contacts).pipe(
tap((_) => { tap((_) => {
this.contacts = contacts; this.contacts = contacts;
}) })

View file

@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarsComponent } from './registrars.component'; import RegistrarsComponent from './registrars.component';
describe('RegistrarsComponent', () => { describe('RegistrarsComponent', () => {
let component: RegistrarsComponent; let component: RegistrarsComponent;

View file

@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SecurityComponent } from './security.component'; import SecurityComponent from './security.component';
describe('SecurityComponent', () => { describe('SecurityComponent', () => {
let component: SecurityComponent; let component: SecurityComponent;

View file

@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component'; import UsersComponent from './users.component';
describe('UsersComponent', () => { describe('UsersComponent', () => {
let component: UsersComponent; let component: UsersComponent;

View file

@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WhoisComponent } from './whois.component'; import WhoisComponent from './whois.component';
describe('WhoisComponent', () => { describe('WhoisComponent', () => {
let component: WhoisComponent; let component: WhoisComponent;

View file

@ -41,35 +41,11 @@ export class BackendService {
} }
getContacts(registrarId: string): Observable<Contact[]> { getContacts(registrarId: string): Observable<Contact[]> {
const mockData = [
{
name: 'Name Lastname',
emailAddress: 'test@google.com',
registrarId: 'zoomco',
types: ['ADMIN'],
visibleInWhoisAsAdmin: false,
visibleInWhoisAsTech: false,
visibleInDomainWhoisAsAbuse: false,
},
{
name: 'Testname testlastname',
emailAddress: 'testasd@google.com',
registrarId: 'zoomco',
visibleInWhoisAsAdmin: false,
visibleInWhoisAsTech: false,
visibleInDomainWhoisAsAbuse: false,
types: ['BILLING'],
},
];
return this.http return this.http
.get<Contact[]>( .get<Contact[]>(
`/console-api/settings/contacts?registrarId=${registrarId}` `/console-api/settings/contacts?registrarId=${registrarId}`
) )
.pipe( .pipe(catchError((err) => this.errorCatcher<Contact[]>(err)));
catchError((err) =>
this.errorCatcher<Contact[]>(err, <Contact[]>mockData)
)
);
} }
postContacts( postContacts(
@ -81,4 +57,10 @@ export class BackendService {
{ contacts } { contacts }
); );
} }
getRegistrars(): Observable<string[]> {
return this.http
.get<string[]>('/console-api/registrars')
.pipe(catchError((err) => this.errorCatcher<string[]>(err)));
}
} }