mirror of
https://github.com/google/nomulus.git
synced 2025-07-25 20:18:34 +02:00
Restyle registrar console based on the new design proposal (#2336)
This commit is contained in:
parent
de3af34b66
commit
59f4129ee0
119 changed files with 3468 additions and 2846 deletions
|
@ -73,10 +73,10 @@
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "console-webapp:build:production"
|
"buildTarget": "console-webapp:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"browserTarget": "console-webapp:build:development"
|
"buildTarget": "console-webapp:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "console-webapp:build"
|
"buildTarget": "console-webapp:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
1614
console-webapp/package-lock.json
generated
1614
console-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,69 +13,106 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { Route, RouterModule } from '@angular/router';
|
||||||
import { TldsComponent } from './tlds/tlds.component';
|
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
|
||||||
import { HomeComponent } from './home/home.component';
|
|
||||||
import { SettingsComponent } from './settings/settings.component';
|
|
||||||
import SettingsContactComponent from './settings/contact/contact.component';
|
|
||||||
import SettingsWhoisComponent from './settings/whois/whois.component';
|
|
||||||
import SettingsUsersComponent from './settings/users/users.component';
|
|
||||||
import SettingsSecurityComponent from './settings/security/security.component';
|
|
||||||
import { RegistrarGuard } from './registrar/registrar.guard';
|
|
||||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
|
||||||
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
|
|
||||||
import ContactComponent from './settings/contact/contact.component';
|
|
||||||
import WhoisComponent from './settings/whois/whois.component';
|
|
||||||
import SecurityComponent from './settings/security/security.component';
|
|
||||||
import UsersComponent from './settings/users/users.component';
|
|
||||||
import { DomainListComponent } from './domains/domainList.component';
|
import { DomainListComponent } from './domains/domainList.component';
|
||||||
|
import { HomeComponent } from './home/home.component';
|
||||||
|
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||||
|
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||||
|
import { ResourcesComponent } from './resources/resources.component';
|
||||||
|
import ContactComponent from './settings/contact/contact.component';
|
||||||
|
import SecurityComponent from './settings/security/security.component';
|
||||||
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
|
import UsersComponent from './settings/users/users.component';
|
||||||
|
import WhoisComponent from './settings/whois/whois.component';
|
||||||
|
import { SupportComponent } from './support/support.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
export interface RouteWithIcon extends Route {
|
||||||
|
iconName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routes: RouteWithIcon[] = [
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'registrars', component: RegistrarComponent },
|
{ path: 'registrars', component: RegistrarComponent },
|
||||||
{ path: 'empty-registrar', component: EmptyRegistrar },
|
{
|
||||||
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
|
path: 'home',
|
||||||
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
|
component: HomeComponent,
|
||||||
|
title: 'Dashboard',
|
||||||
|
iconName: 'view_comfy_alt',
|
||||||
|
},
|
||||||
|
// { path: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" },
|
||||||
{
|
{
|
||||||
path: DomainListComponent.PATH,
|
path: DomainListComponent.PATH,
|
||||||
component: DomainListComponent,
|
component: DomainListComponent,
|
||||||
canActivate: [RegistrarGuard],
|
title: 'Domains',
|
||||||
|
iconName: 'view_list',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: SettingsComponent.PATH,
|
path: SettingsComponent.PATH,
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
|
title: 'Settings',
|
||||||
|
iconName: 'settings',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: 'registrars',
|
redirectTo: ContactComponent.PATH,
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ContactComponent.PATH,
|
path: ContactComponent.PATH,
|
||||||
component: SettingsContactComponent,
|
component: ContactComponent,
|
||||||
canActivate: [RegistrarGuard],
|
title: 'Contacts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: WhoisComponent.PATH,
|
path: WhoisComponent.PATH,
|
||||||
component: SettingsWhoisComponent,
|
component: WhoisComponent,
|
||||||
canActivate: [RegistrarGuard],
|
title: 'WHOIS Info',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: SecurityComponent.PATH,
|
path: SecurityComponent.PATH,
|
||||||
component: SettingsSecurityComponent,
|
component: SecurityComponent,
|
||||||
canActivate: [RegistrarGuard],
|
title: 'Security',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: UsersComponent.PATH,
|
path: UsersComponent.PATH,
|
||||||
component: SettingsUsersComponent,
|
component: UsersComponent,
|
||||||
canActivate: [RegistrarGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: RegistrarComponent.PATH,
|
|
||||||
component: RegistrarComponent,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// path: EppConsole.PATH,
|
||||||
|
// component: EppConsoleComponent,
|
||||||
|
// title: "EPP Console",
|
||||||
|
// iconName: "upgrade"
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
path: RegistrarComponent.PATH,
|
||||||
|
component: RegistrarComponent,
|
||||||
|
title: 'Registrars',
|
||||||
|
iconName: 'account_circle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: RegistrarDetailsComponent.PATH,
|
||||||
|
component: RegistrarDetailsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: BillingInfoComponent.PATH,
|
||||||
|
component: BillingInfoComponent,
|
||||||
|
title: 'Billing Info',
|
||||||
|
iconName: 'credit_card',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ResourcesComponent.PATH,
|
||||||
|
component: ResourcesComponent,
|
||||||
|
title: 'Resources',
|
||||||
|
iconName: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: SupportComponent.PATH,
|
||||||
|
component: SupportComponent,
|
||||||
|
title: 'Support',
|
||||||
|
iconName: 'help',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
<div class="console-app">
|
<div class="console-app mat-typography">
|
||||||
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
|
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
|
||||||
<mat-sidenav-container class="console-app__container">
|
<mat-sidenav-container class="console-app__container">
|
||||||
<mat-sidenav #sidenav class="console-app__sidebar">
|
<mat-sidenav
|
||||||
<mat-nav-list>
|
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
|
||||||
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
|
[opened]="!breakpointObserver.isMobileView()"
|
||||||
Home page
|
#sidenav
|
||||||
</a>
|
class="console-app__sidebar"
|
||||||
<a mat-list-item [routerLink]="'/tlds'" routerLinkActive="active">
|
>
|
||||||
TLDS
|
<app-navigation />
|
||||||
</a>
|
|
||||||
<a mat-list-item [routerLink]="'/settings'" routerLinkActive="active">
|
|
||||||
Settings
|
|
||||||
</a>
|
|
||||||
</mat-nav-list>
|
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content class="console-app__content-wrapper">
|
<mat-sidenav-content class="console-app__content-wrapper">
|
||||||
<div *ngIf="globalLoader.isLoading" class="console-app__global-spinner">
|
<div *ngIf="globalLoader.isLoading" class="console-app__global-spinner">
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
</div>
|
</div>
|
||||||
<div class="console-app__content" *ngIf="renderRouter">
|
<div class="console-app__content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,8 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
font-family: Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
|
font-family: "Google Sans", Roboto, Helvetica, Arial, sans-serif,
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol" !important;
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -26,26 +26,18 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
&__container {
|
&__container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-top: -12px;
|
|
||||||
padding-bottom: 36px;
|
padding-bottom: 36px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
&__sidebar {
|
&__sidebar {
|
||||||
min-width: 300px;
|
min-width: 240px;
|
||||||
a::before {
|
border: 0;
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.active {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__content-wrapper {
|
|
||||||
margin: 12px 24px;
|
|
||||||
}
|
}
|
||||||
&__content {
|
&__content {
|
||||||
max-width: 1340px;
|
padding: 0 16px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
&__global-spinner {
|
&__global-spinner {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,12 +12,13 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { AfterViewInit, Component, ViewChild, effect } from '@angular/core';
|
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||||
import { RegistrarService } from './registrar/registrar.service';
|
|
||||||
import { UserDataService } from './shared/services/userData.service';
|
|
||||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
|
||||||
import { MatSidenav } from '@angular/material/sidenav';
|
import { MatSidenav } from '@angular/material/sidenav';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { RegistrarService } from './registrar/registrar.service';
|
||||||
|
import { BreakPointObserverService } from './shared/services/breakPoint.service';
|
||||||
|
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||||
|
import { UserDataService } from './shared/services/userData.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
@ -25,32 +26,32 @@ import { MatSidenav } from '@angular/material/sidenav';
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppComponent implements AfterViewInit {
|
export class AppComponent implements AfterViewInit {
|
||||||
renderRouter: boolean = true;
|
@ViewChild(MatSidenav)
|
||||||
|
|
||||||
@ViewChild('sidenav')
|
|
||||||
sidenav!: MatSidenav;
|
sidenav!: MatSidenav;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected registrarService: RegistrarService,
|
protected registrarService: RegistrarService,
|
||||||
protected userDataService: UserDataService,
|
protected userDataService: UserDataService,
|
||||||
protected globalLoader: GlobalLoaderService,
|
protected globalLoader: GlobalLoaderService,
|
||||||
protected router: Router
|
protected breakpointObserver: BreakPointObserverService,
|
||||||
) {
|
private router: Router
|
||||||
effect(() => {
|
) {}
|
||||||
if (registrarService.registrarId()) {
|
|
||||||
this.renderRouter = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.renderRouter = true;
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.subscribe((event) => {
|
||||||
if (event instanceof NavigationEnd) {
|
if (event instanceof NavigationEnd) {
|
||||||
this.sidenav.close();
|
if (this.breakpointObserver.isMobileView()) {
|
||||||
|
this.sidenav.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSidenav() {
|
||||||
|
if (this.sidenav.opened) {
|
||||||
|
this.sidenav.close();
|
||||||
|
} else {
|
||||||
|
this.sidenav.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,71 +13,68 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import { MaterialModule } from './material.module';
|
import { MaterialModule } from './material.module';
|
||||||
|
|
||||||
import { BackendService } from './shared/services/backend.service';
|
import { BackendService } from './shared/services/backend.service';
|
||||||
|
|
||||||
import { HomeComponent } from './home/home.component';
|
|
||||||
import { TldsComponent } from './tlds/tlds.component';
|
|
||||||
import { HeaderComponent } from './header/header.component';
|
|
||||||
import { SettingsComponent } from './settings/settings.component';
|
|
||||||
import SettingsContactComponent, {
|
|
||||||
ContactDetailsDialogComponent,
|
|
||||||
} from './settings/contact/contact.component';
|
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
|
||||||
import { RegistrarGuard } from './registrar/registrar.guard';
|
|
||||||
import SecurityComponent from './settings/security/security.component';
|
|
||||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||||
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
|
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
|
||||||
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
|
|
||||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
|
||||||
import { ContactWidgetComponent } from './home/widgets/contactWidget.component';
|
|
||||||
import { PromotionsWidgetComponent } from './home/widgets/promotionsWidget.component';
|
|
||||||
import { TldsWidgetComponent } from './home/widgets/tldsWidget.component';
|
|
||||||
import { ResourcesWidgetComponent } from './home/widgets/resourcesWidget.component';
|
|
||||||
import { EppWidgetComponent } from './home/widgets/eppWidget.component';
|
|
||||||
import { BillingWidgetComponent } from './home/widgets/billingWidget.component';
|
|
||||||
import { DomainsWidgetComponent } from './home/widgets/domainsWidget.component';
|
|
||||||
import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component';
|
|
||||||
import { UserDataService } from './shared/services/userData.service';
|
|
||||||
import WhoisComponent from './settings/whois/whois.component';
|
|
||||||
import { SnackBarModule } from './snackbar.module';
|
|
||||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
|
||||||
import { DomainListComponent } from './domains/domainList.component';
|
import { DomainListComponent } from './domains/domainList.component';
|
||||||
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
|
import { HeaderComponent } from './header/header.component';
|
||||||
|
import { HomeComponent } from './home/home.component';
|
||||||
|
import { NavigationComponent } from './navigation/navigation.component';
|
||||||
|
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||||
|
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
|
||||||
|
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||||
|
import { ResourcesComponent } from './resources/resources.component';
|
||||||
|
import SettingsContactComponent from './settings/contact/contact.component';
|
||||||
|
import { ContactDetailsComponent } from './settings/contact/contactDetails.component';
|
||||||
|
import SecurityComponent from './settings/security/security.component';
|
||||||
|
import SecurityEditComponent from './settings/security/securityEdit.component';
|
||||||
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
|
import WhoisComponent from './settings/whois/whois.component';
|
||||||
|
import WhoisEditComponent from './settings/whois/whoisEdit.component';
|
||||||
|
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
|
||||||
|
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
|
||||||
|
import { LocationBackDirective } from './shared/directives/locationBack.directive';
|
||||||
|
import { BreakPointObserverService } from './shared/services/breakPoint.service';
|
||||||
|
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||||
|
import { UserDataService } from './shared/services/userData.service';
|
||||||
|
import { SnackBarModule } from './snackbar.module';
|
||||||
|
import { SupportComponent } from './support/support.component';
|
||||||
|
import { TldsComponent } from './tlds/tlds.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
DialogBottomSheetWrapper,
|
BillingInfoComponent,
|
||||||
BillingWidgetComponent,
|
ContactDetailsComponent,
|
||||||
ContactDetailsDialogComponent,
|
|
||||||
ContactWidgetComponent,
|
|
||||||
DomainListComponent,
|
DomainListComponent,
|
||||||
DomainsWidgetComponent,
|
|
||||||
EmptyRegistrar,
|
|
||||||
EppWidgetComponent,
|
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
HomeComponent,
|
HomeComponent,
|
||||||
PromotionsWidgetComponent,
|
LocationBackDirective,
|
||||||
|
NavigationComponent,
|
||||||
|
NotificationsComponent,
|
||||||
RegistrarComponent,
|
RegistrarComponent,
|
||||||
RegistrarDetailsComponent,
|
RegistrarDetailsComponent,
|
||||||
RegistrarSelectorComponent,
|
RegistrarSelectorComponent,
|
||||||
ResourcesWidgetComponent,
|
ResourcesComponent,
|
||||||
SecurityComponent,
|
SecurityComponent,
|
||||||
|
SecurityEditComponent,
|
||||||
|
SelectedRegistrarWrapper,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
SettingsContactComponent,
|
SettingsContactComponent,
|
||||||
SettingsWidgetComponent,
|
SupportComponent,
|
||||||
TldsComponent,
|
TldsComponent,
|
||||||
TldsWidgetComponent,
|
|
||||||
WhoisComponent,
|
WhoisComponent,
|
||||||
|
WhoisEditComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
|
@ -90,8 +87,8 @@ import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
BackendService,
|
BackendService,
|
||||||
|
BreakPointObserverService,
|
||||||
GlobalLoaderService,
|
GlobalLoaderService,
|
||||||
RegistrarGuard,
|
|
||||||
UserDataService,
|
UserDataService,
|
||||||
{
|
{
|
||||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<app-selected-registrar-wrapper>
|
||||||
|
<h1 class="mat-headline-4">Billing Info</h1>
|
||||||
|
<div class="console-app__billing">
|
||||||
|
<div>
|
||||||
|
<div class="console-app__billing-subhead">
|
||||||
|
Billing records and information
|
||||||
|
</div>
|
||||||
|
<a class="text-l" href="{{ driveFolderUrl() }}" target="_blank"
|
||||||
|
>View on Google Drive</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="./assets/billing.png" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-selected-registrar-wrapper>
|
|
@ -0,0 +1,22 @@
|
||||||
|
.console-app__billing {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap-reverse;
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 300px;
|
||||||
|
min-width: 200px;
|
||||||
|
margin-top: -40px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&-subhead {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,27 +12,29 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, computed } from '@angular/core';
|
||||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
import { RegistrarService } from '../registrar/registrar.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[app-billing-widget]',
|
selector: 'app-billingInfo',
|
||||||
templateUrl: './billingWidget.component.html',
|
templateUrl: './billingInfo.component.html',
|
||||||
|
styleUrls: ['./billingInfo.component.scss'],
|
||||||
})
|
})
|
||||||
export class BillingWidgetComponent {
|
export class BillingInfoComponent {
|
||||||
|
public static PATH = 'billingInfo';
|
||||||
constructor(public registrarService: RegistrarService) {}
|
constructor(public registrarService: RegistrarService) {}
|
||||||
|
|
||||||
public get driveFolderUrl(): string {
|
driveFolderUrl = computed<string>(() => {
|
||||||
if (this.registrarService.registrar()?.driveFolderId) {
|
if (this.registrarService.registrar()?.driveFolderId) {
|
||||||
return `https://drive.google.com/drive/folders/${
|
return `https://drive.google.com/drive/folders/${
|
||||||
this.registrarService.registrar()?.driveFolderId
|
this.registrarService.registrar()?.driveFolderId
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
});
|
||||||
|
|
||||||
openBillingDetails(e: MouseEvent) {
|
openBillingDetails(e: MouseEvent) {
|
||||||
if (!this.driveFolderUrl) {
|
if (!this.driveFolderUrl()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,52 +1,65 @@
|
||||||
<div class="console-domains">
|
<app-selected-registrar-wrapper>
|
||||||
<mat-form-field>
|
<h1 class="mat-headline-4">Domains</h1>
|
||||||
<mat-label>Filter</mat-label>
|
<div class="console-domains">
|
||||||
<input
|
@if (totalResults === 0) {
|
||||||
type="search"
|
<div class="console-app__empty-domains">
|
||||||
matInput
|
<h1>
|
||||||
[(ngModel)]="searchTerm"
|
<mat-icon class="console-app__empty-domains-icon secondary-text"
|
||||||
(ngModelChange)="sendInput()"
|
>apps_outage</mat-icon
|
||||||
#input
|
>
|
||||||
/>
|
</h1>
|
||||||
</mat-form-field>
|
<h1>No domains found</h1>
|
||||||
|
</div>
|
||||||
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
|
} @else if(isLoading) {
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
</div>
|
} @else {
|
||||||
<ng-template #domains_content>
|
<mat-form-field class="console-app__domains-filter">
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
<mat-label>Filter</mat-label>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="searchTerm"
|
||||||
|
(ngModelChange)="sendInput()"
|
||||||
|
#input
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
class="mat-elevation-z0"
|
||||||
|
class="console-app__domains-table"
|
||||||
|
>
|
||||||
<ng-container matColumnDef="domainName">
|
<ng-container matColumnDef="domainName">
|
||||||
<th mat-header-cell *matHeaderCellDef>Domain Name</th>
|
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
|
||||||
<td mat-cell *matCellDef="let element">{{ element.domainName }}</td>
|
<mat-cell *matCellDef="let element">{{ element.domainName }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="creationTime">
|
<ng-container matColumnDef="creationTime">
|
||||||
<th mat-header-cell *matHeaderCellDef>Creation Time</th>
|
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
|
||||||
<td mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
{{ element.creationTime.creationTime }}
|
{{ element.creationTime.creationTime }}
|
||||||
</td>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="registrationExpirationTime">
|
<ng-container matColumnDef="registrationExpirationTime">
|
||||||
<th mat-header-cell *matHeaderCellDef>Expiration Time</th>
|
<mat-header-cell *matHeaderCellDef>Expiration Time</mat-header-cell>
|
||||||
<td mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
{{ element.registrationExpirationTime }}
|
{{ element.registrationExpirationTime }}
|
||||||
</td>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="statuses">
|
<ng-container matColumnDef="statuses">
|
||||||
<th mat-header-cell *matHeaderCellDef>Statuses</th>
|
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
|
||||||
<td mat-cell *matCellDef="let element">{{ element.statuses }}</td>
|
<mat-cell *matCellDef="let element">{{ element.statuses }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||||
|
|
||||||
<!-- Row shown when there is no matching data. -->
|
<!-- Row shown when there is no matching data. -->
|
||||||
<tr class="mat-row" *matNoDataRow>
|
<mat-row *matNoDataRow>
|
||||||
<td class="mat-cell" colspan="4">No domains found</td>
|
<mat-cell colspan="4">No domains found</mat-cell>
|
||||||
</tr>
|
</mat-row>
|
||||||
</table>
|
</mat-table>
|
||||||
<mat-paginator
|
<mat-paginator
|
||||||
[length]="totalResults"
|
[length]="totalResults"
|
||||||
[pageIndex]="pageNumber"
|
[pageIndex]="pageNumber"
|
||||||
|
@ -56,5 +69,6 @@
|
||||||
aria-label="Select page of domain results"
|
aria-label="Select page of domain results"
|
||||||
showFirstLastButtons
|
showFirstLastButtons
|
||||||
></mat-paginator>
|
></mat-paginator>
|
||||||
</ng-template>
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</app-selected-registrar-wrapper>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
.console-app {
|
||||||
|
$min-width: 756px;
|
||||||
|
|
||||||
|
&__empty-domains {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
&-icon {
|
||||||
|
transform: scale(3);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__domains {
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__domains-filter {
|
||||||
|
min-width: $min-width !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__domains-table {
|
||||||
|
min-width: $min-width !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-paginator {
|
||||||
|
min-width: $min-width !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,13 +12,15 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ViewChild } from '@angular/core';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { Component, ViewChild, effect } from '@angular/core';
|
||||||
import { BackendService } from '../shared/services/backend.service';
|
|
||||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
import { RegistrarService } from '../registrar/registrar.service';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Domain, DomainListService } from './domainList.service';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Subject, debounceTime } from 'rxjs';
|
import { Subject, debounceTime } from 'rxjs';
|
||||||
|
import { RegistrarService } from '../registrar/registrar.service';
|
||||||
|
import { BackendService } from '../shared/services/backend.service';
|
||||||
|
import { Domain, DomainListService } from './domainList.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-domain-list',
|
selector: 'app-domain-list',
|
||||||
|
@ -45,15 +47,22 @@ export class DomainListComponent {
|
||||||
|
|
||||||
pageNumber?: number;
|
pageNumber?: number;
|
||||||
resultsPerPage = 50;
|
resultsPerPage = 50;
|
||||||
totalResults?: number;
|
totalResults?: number = 0;
|
||||||
|
|
||||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private backendService: BackendService,
|
private backendService: BackendService,
|
||||||
private domainListService: DomainListService,
|
private domainListService: DomainListService,
|
||||||
private registrarService: RegistrarService
|
private registrarService: RegistrarService,
|
||||||
) {}
|
private _snackBar: MatSnackBar
|
||||||
|
) {
|
||||||
|
effect(() => {
|
||||||
|
if (this.registrarService.registrarId()) {
|
||||||
|
this.reloadData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dataSource.paginator = this.paginator;
|
this.dataSource.paginator = this.paginator;
|
||||||
|
@ -63,7 +72,6 @@ export class DomainListComponent {
|
||||||
.subscribe((searchTermValue) => {
|
.subscribe((searchTermValue) => {
|
||||||
this.reloadData();
|
this.reloadData();
|
||||||
});
|
});
|
||||||
this.reloadData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -79,10 +87,16 @@ export class DomainListComponent {
|
||||||
this.totalResults,
|
this.totalResults,
|
||||||
this.searchTerm
|
this.searchTerm
|
||||||
)
|
)
|
||||||
.subscribe((domainListResult) => {
|
.subscribe({
|
||||||
this.dataSource.data = domainListResult.domains;
|
error: (err: HttpErrorResponse) => {
|
||||||
this.totalResults = domainListResult.totalResults;
|
this._snackBar.open(err.message);
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
next: (domainListResult) => {
|
||||||
|
this.dataSource.data = (domainListResult || {}).domains;
|
||||||
|
this.totalResults = (domainListResult || {}).totalResults || 0;
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,9 +13,9 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { BackendService } from '../shared/services/backend.service';
|
|
||||||
import { RegistrarService } from '../registrar/registrar.service';
|
|
||||||
import { tap } from 'rxjs';
|
import { tap } from 'rxjs';
|
||||||
|
import { RegistrarService } from '../registrar/registrar.service';
|
||||||
|
import { BackendService } from '../shared/services/backend.service';
|
||||||
|
|
||||||
export interface CreateAutoTimestamp {
|
export interface CreateAutoTimestamp {
|
||||||
creationTime: string;
|
creationTime: string;
|
||||||
|
@ -61,7 +61,7 @@ export class DomainListService {
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((domainListResult: DomainListResult) => {
|
tap((domainListResult: DomainListResult) => {
|
||||||
this.checkpointTime = domainListResult.checkpointTime;
|
this.checkpointTime = domainListResult?.checkpointTime;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -16,25 +16,27 @@
|
||||||
&__logo {
|
&__logo {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
&__menu-btn {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
&__header {
|
&__header {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 15px;
|
||||||
@media (max-width: 599px) {
|
|
||||||
.mat-toolbar {
|
.mat-toolbar {
|
||||||
padding: 0;
|
background: transparent;
|
||||||
}
|
margin-bottom: 10px;
|
||||||
.console-app__logo {
|
}
|
||||||
font-size: 16px;
|
|
||||||
}
|
@media (max-width: 480px) {
|
||||||
button {
|
}
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
&-user-icon {
|
||||||
width: 30px;
|
margin-left: 20px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, EventEmitter, Output } from '@angular/core';
|
import { Component, EventEmitter, Output } from '@angular/core';
|
||||||
|
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
|
@ -22,6 +23,8 @@ import { Component, EventEmitter, Output } from '@angular/core';
|
||||||
export class HeaderComponent {
|
export class HeaderComponent {
|
||||||
private isNavOpen = false;
|
private isNavOpen = false;
|
||||||
|
|
||||||
|
constructor(protected breakpointObserver: BreakPointObserverService) {}
|
||||||
|
|
||||||
@Output() toggleNavOpen = new EventEmitter<boolean>();
|
@Output() toggleNavOpen = new EventEmitter<boolean>();
|
||||||
|
|
||||||
toggleNavPane() {
|
toggleNavPane() {
|
||||||
|
|
|
@ -1,13 +1,52 @@
|
||||||
<div class="console-app__home">
|
<div
|
||||||
<h1>Welcome to the Google Registry Console</h1>
|
class="console-app__home"
|
||||||
|
[class.console-app__home_tablet]="breakPointObserverService.isTabletView()"
|
||||||
|
>
|
||||||
|
<h1 class="mat-headline-4">Dashboard</h1>
|
||||||
<div class="console-app__home-widgets">
|
<div class="console-app__home-widgets">
|
||||||
<div app-domains-widget class="console-app__widget-wrapper__wide"></div>
|
<mat-card appearance="outlined">
|
||||||
<div app-contact-widget class="console-app__widget-wrapper__wide"></div>
|
<mat-card-content>
|
||||||
<div app-tlds-widget></div>
|
<h3>
|
||||||
<div app-promotions-widget class="console-app__widget-wrapper__wide"></div>
|
<mat-icon class="secondary-text">view_list</mat-icon>
|
||||||
<div app-settings-widget class="console-app__widget-wrapper__wide"></div>
|
DUMs
|
||||||
<div app-resources-widget></div>
|
</h3>
|
||||||
<div app-billing-widget></div>
|
<p class="secondary-text">View Domains</p>
|
||||||
<div app-epp-widget></div>
|
</mat-card-content>
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-button color="primary" (click)="viewDums()">
|
||||||
|
View DUMs
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<h3>
|
||||||
|
<mat-icon class="secondary-text">settings</mat-icon>
|
||||||
|
EPP Passwords
|
||||||
|
</h3>
|
||||||
|
<p class="secondary-text">View / Update EPP Password</p>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-button color="primary" (click)="updateEppPassword()">
|
||||||
|
Update EPP Password
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<h3>
|
||||||
|
<mat-icon class="secondary-text">account_circle</mat-icon>
|
||||||
|
Registrars
|
||||||
|
</h3>
|
||||||
|
<p class="secondary-text">View all registrars</p>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-button color="primary" (click)="viewRegistrars()">
|
||||||
|
Manage Registrars
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,25 +13,23 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
.console-app {
|
.console-app {
|
||||||
&__home {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
&__home-widgets {
|
&__home-widgets {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
gap: 1rem;
|
||||||
grid-gap: 20px;
|
h3 {
|
||||||
grid-auto-flow: dense;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
mat-card {
|
mat-card {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
h1 {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 510px) {
|
&__home_tablet {
|
||||||
.console-app__widget-wrapper__wide {
|
.console-app__home-widgets {
|
||||||
grid-column: initial;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,12 +12,38 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ViewEncapsulation } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DomainListComponent } from '../domains/domainList.component';
|
||||||
|
import { RegistrarComponent } from '../registrar/registrarsTable.component';
|
||||||
|
import SecurityComponent from '../settings/security/security.component';
|
||||||
|
import { SettingsComponent } from '../settings/settings.component';
|
||||||
|
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
styleUrls: ['./home.component.scss'],
|
styleUrls: ['./home.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None,
|
|
||||||
})
|
})
|
||||||
export class HomeComponent {}
|
export class HomeComponent {
|
||||||
|
constructor(
|
||||||
|
protected breakPointObserverService: BreakPointObserverService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
viewRegistrars() {
|
||||||
|
this.router.navigate([RegistrarComponent.PATH], {
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateEppPassword() {
|
||||||
|
this.router.navigate(
|
||||||
|
[SettingsComponent.PATH + '/' + SecurityComponent.PATH],
|
||||||
|
{ queryParamsHandling: 'merge' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
viewDums() {
|
||||||
|
this.router.navigate([DomainListComponent.PATH], {
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<mat-card>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<a
|
|
||||||
class="console-app__widget_left"
|
|
||||||
href="{{ driveFolderUrl }}"
|
|
||||||
(click)="openBillingDetails($event)"
|
|
||||||
>
|
|
||||||
<mat-icon class="console-app__widget-icon">account_balance</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">Billing Info</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
<span *ngIf="driveFolderUrl; else noDriveFolderUrl">
|
|
||||||
View important billing and payments information.
|
|
||||||
</span>
|
|
||||||
<ng-template #noDriveFolderUrl>
|
|
||||||
<span> Your billing folder is pending allocation. </span>
|
|
||||||
</ng-template>
|
|
||||||
</h4>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<mat-card>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<div class="console-app__widget_left">
|
|
||||||
<mat-icon class="console-app__widget-icon">call</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">Contact Support</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
Let us know if you have any questions
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="console-app__widget_right">
|
|
||||||
<div class="console-app__widget-section-header">Give us a Call</div>
|
|
||||||
<p class="secondary-text">
|
|
||||||
Call {{ userDataService.userData?.productName }} support at
|
|
||||||
<a href="tel:{{ userDataService.userData?.supportPhoneNumber }}">{{
|
|
||||||
userDataService.userData?.supportPhoneNumber
|
|
||||||
}}</a>
|
|
||||||
</p>
|
|
||||||
<div class="console-app__widget-section-header">Send us an Email</div>
|
|
||||||
<p class="secondary-text">
|
|
||||||
Email {{ userDataService.userData?.productName }} at
|
|
||||||
<a href="mailto:{{ userDataService.userData?.supportEmail }}">{{
|
|
||||||
userDataService.userData?.supportEmail
|
|
||||||
}}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,24 +0,0 @@
|
||||||
// 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 { UserDataService } from 'src/app/shared/services/userData.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: '[app-contact-widget]',
|
|
||||||
templateUrl: './contactWidget.component.html',
|
|
||||||
})
|
|
||||||
export class ContactWidgetComponent {
|
|
||||||
constructor(public userDataService: UserDataService) {}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
<mat-card>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<div class="console-app__widget_left">
|
|
||||||
<mat-icon class="console-app__widget-icon">view_list</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">Domains</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
Manage domain names and registry lock settings.
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="console-app__widget_right">
|
|
||||||
<button mat-button color="primary" class="console-app__widget-link">
|
|
||||||
Create a Domain
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">Register a new domain name</p>
|
|
||||||
<button
|
|
||||||
mat-button
|
|
||||||
color="primary"
|
|
||||||
class="console-app__widget-link"
|
|
||||||
(click)="openDomainsPage()"
|
|
||||||
>
|
|
||||||
View DUMs
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">
|
|
||||||
Download a csv of all domains under management
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<mat-card>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<div class="console-app__widget_left">
|
|
||||||
<mat-icon class="console-app__widget-icon">computer</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">EPP Console</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
Test run and execute EPP commands using XML files.
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,27 +0,0 @@
|
||||||
<mat-card class="console-app__widget-wrapper__wide">
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<div class="console-app__widget_left">
|
|
||||||
<mat-icon class="console-app__widget-icon">subject</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">Promotions</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
Manage Google Registry promotions and view onboarding process.
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="console-app__widget_right">
|
|
||||||
<button mat-button color="primary" class="console-app__widget-link">
|
|
||||||
Manage Preferred Partner Program
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">
|
|
||||||
Onboard or view current preferred partner status
|
|
||||||
</p>
|
|
||||||
<button mat-button color="primary" class="console-app__widget-link">
|
|
||||||
Manage Registry Lock Program
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">
|
|
||||||
Onboard or view current registry lock status
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,17 +0,0 @@
|
||||||
<mat-card>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<a
|
|
||||||
class="console-app__widget_left"
|
|
||||||
href="{{ userDataService.userData?.technicalDocsUrl }}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">Resources</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
Use Google Drive to view onboarding FAQs, and technical documentation.
|
|
||||||
</h4>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,53 +0,0 @@
|
||||||
<mat-card class="console-app__widget-wrapper__wide">
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<div class="console-app__widget_left">
|
|
||||||
<mat-icon class="console-app__widget-icon">settings</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">Settings</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
Configure registrar settings, manage console users, and view activity
|
|
||||||
log.
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="console-app__widget_right">
|
|
||||||
<button
|
|
||||||
mat-button
|
|
||||||
color="primary"
|
|
||||||
class="console-app__widget-link"
|
|
||||||
(click)="openContactsPage()"
|
|
||||||
>
|
|
||||||
Contact Information
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">Manage Primary, Technical, etc contacts.</p>
|
|
||||||
<button
|
|
||||||
mat-button
|
|
||||||
color="primary"
|
|
||||||
class="console-app__widget-link"
|
|
||||||
(click)="openSecurityPage()"
|
|
||||||
>
|
|
||||||
Security
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">
|
|
||||||
Manage IP allow lists and SSL certificates.
|
|
||||||
</p>
|
|
||||||
<button mat-button color="primary" class="console-app__widget-link">
|
|
||||||
Nomulus Password
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">Reset your Nomulus password.</p>
|
|
||||||
<button mat-button color="primary" class="console-app__widget-link">
|
|
||||||
User Management
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">Create and manage console user accounts</p>
|
|
||||||
<button
|
|
||||||
mat-button
|
|
||||||
color="primary"
|
|
||||||
class="console-app__widget-link"
|
|
||||||
(click)="openRegistrarsPage()"
|
|
||||||
>
|
|
||||||
Registrar Management
|
|
||||||
</button>
|
|
||||||
<p class="secondary-text">Create and manage registrar accounts</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,44 +0,0 @@
|
||||||
// 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 { Router } from '@angular/router';
|
|
||||||
import { RegistrarComponent } from 'src/app/registrar/registrarsTable.component';
|
|
||||||
import ContactComponent from 'src/app/settings/contact/contact.component';
|
|
||||||
import SecurityComponent from 'src/app/settings/security/security.component';
|
|
||||||
import { SettingsComponent } from 'src/app/settings/settings.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: '[app-settings-widget]',
|
|
||||||
templateUrl: './settingsWidget.component.html',
|
|
||||||
})
|
|
||||||
export class SettingsWidgetComponent {
|
|
||||||
constructor(private router: Router) {}
|
|
||||||
|
|
||||||
openRegistrarsPage() {
|
|
||||||
this.navigate(RegistrarComponent.PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
openSecurityPage() {
|
|
||||||
this.navigate(SecurityComponent.PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
openContactsPage() {
|
|
||||||
this.navigate(ContactComponent.PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
private navigate(route: string) {
|
|
||||||
this.router.navigate([SettingsComponent.PATH, route]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
<mat-card>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="console-app__widget">
|
|
||||||
<div class="console-app__widget_left">
|
|
||||||
<mat-icon class="console-app__widget-icon">list_alt</mat-icon>
|
|
||||||
<h1 class="console-app__widget-title">TLDs</h1>
|
|
||||||
<h4 class="secondary-text text-center">
|
|
||||||
View launch phase information for all onboarded and available TLDs.
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -46,6 +46,7 @@ import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -83,6 +84,7 @@ import { MatChipsModule } from '@angular/material/chips';
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MaterialModule {}
|
export class MaterialModule {}
|
||||||
|
|
44
console-webapp/src/app/navigation/navigation.component.html
Normal file
44
console-webapp/src/app/navigation/navigation.component.html
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<mat-tree
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
[treeControl]="treeControl"
|
||||||
|
class="console-app__nav-tree"
|
||||||
|
>
|
||||||
|
<mat-tree-node
|
||||||
|
*matTreeNodeDef="let node"
|
||||||
|
matTreeNodeToggle
|
||||||
|
(click)="onClick(node)"
|
||||||
|
[class.active]="router.url.endsWith(node.path)"
|
||||||
|
>
|
||||||
|
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
|
||||||
|
{{ node.iconName }}
|
||||||
|
</mat-icon>
|
||||||
|
{{ node.title }}
|
||||||
|
</mat-tree-node>
|
||||||
|
<mat-nested-tree-node
|
||||||
|
*matTreeNodeDef="let node; when: hasChild"
|
||||||
|
(click)="onClick(node)"
|
||||||
|
>
|
||||||
|
<div class="mat-tree-node" [class.active]="router.url.endsWith(node.path)">
|
||||||
|
<button
|
||||||
|
class="console-app__nav-icon_expand"
|
||||||
|
mat-icon-button
|
||||||
|
matTreeNodeToggle
|
||||||
|
[attr.aria-label]="'Toggle ' + node.title"
|
||||||
|
>
|
||||||
|
<mat-icon>
|
||||||
|
{{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }}
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
|
||||||
|
{{ node.iconName }}
|
||||||
|
</mat-icon>
|
||||||
|
{{ node.title }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
[class.console-app__nav-tree_invisible]="!treeControl.isExpanded(node)"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<ng-container matTreeNodeOutlet></ng-container>
|
||||||
|
</div>
|
||||||
|
</mat-nested-tree-node>
|
||||||
|
</mat-tree>
|
71
console-webapp/src/app/navigation/navigation.component.scss
Normal file
71
console-webapp/src/app/navigation/navigation.component.scss
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
$expand-icon-size: 26px;
|
||||||
|
|
||||||
|
.console-app {
|
||||||
|
&__sidebar {
|
||||||
|
min-width: 300px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__nav-icon {
|
||||||
|
width: $expand-icon-size;
|
||||||
|
height: $expand-icon-size;
|
||||||
|
color: var(--secondary) !important;
|
||||||
|
margin-right: $expand-icon-size;
|
||||||
|
|
||||||
|
&_expand {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0px;
|
||||||
|
width: $expand-icon-size;
|
||||||
|
height: $expand-icon-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__nav-tree {
|
||||||
|
.mat-tree-node {
|
||||||
|
cursor: pointer;
|
||||||
|
padding-left: $expand-icon-size;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--light-highlight);
|
||||||
|
border-radius: 0 25px 25px 0;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
border-radius: 0 25px 25px 0;
|
||||||
|
background-color: var(--lightest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div[role="group"] > .mat-tree-node {
|
||||||
|
// expand icon + regular icon + spacing = 3 * $expand-icon-size
|
||||||
|
padding-left: calc($expand-icon-size * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
console-webapp/src/app/navigation/navigation.component.ts
Normal file
107
console-webapp/src/app/navigation/navigation.component.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// 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 { NestedTreeControl } from '@angular/cdk/tree';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { RouteWithIcon, routes } from '../app-routing.module';
|
||||||
|
|
||||||
|
interface NavMenuNode extends RouteWithIcon {
|
||||||
|
parentRoute?: RouteWithIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is responsible for rendering navigation menu based on the allowed routes
|
||||||
|
* and keeping UI in sync when route changes (eg highlights selected route in the menu).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-navigation',
|
||||||
|
templateUrl: './navigation.component.html',
|
||||||
|
styleUrls: ['./navigation.component.scss'],
|
||||||
|
})
|
||||||
|
export class NavigationComponent {
|
||||||
|
renderRouter: boolean = true;
|
||||||
|
treeControl = new NestedTreeControl<RouteWithIcon>((node) => node.children);
|
||||||
|
dataSource = new MatTreeNestedDataSource<RouteWithIcon>();
|
||||||
|
private subscription!: Subscription;
|
||||||
|
hasChild = (_: number, node: RouteWithIcon) =>
|
||||||
|
!!node.children && node.children.length > 0;
|
||||||
|
|
||||||
|
constructor(protected router: Router) {
|
||||||
|
this.dataSource.data = this.ngRoutesToNavMenuNodes(routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.subscription = this.router.events.subscribe((navigationParams) => {
|
||||||
|
if (navigationParams instanceof NavigationEnd) {
|
||||||
|
this.syncExpandedNavigationWithRoute(navigationParams.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncExpandedNavigationWithRoute(url: string) {
|
||||||
|
const maybeComponentWithChildren = this.dataSource.data.find((menuNode) => {
|
||||||
|
return (
|
||||||
|
// @ts-ignore - optional function added to components with children,
|
||||||
|
// there's no availble tools to get current active router component
|
||||||
|
typeof menuNode.component?.matchesUrl === 'function' &&
|
||||||
|
// @ts-ignore
|
||||||
|
menuNode.component?.matchesUrl(url)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (maybeComponentWithChildren) {
|
||||||
|
this.treeControl.expand(maybeComponentWithChildren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(node: NavMenuNode) {
|
||||||
|
if (node.parentRoute) {
|
||||||
|
this.router.navigate([node.parentRoute.path + '/' + node.path], {
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.router.navigate([node.path], { queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only want to use routes with titles and we want to provide easy reference to parent node
|
||||||
|
*/
|
||||||
|
ngRoutesToNavMenuNodes(routes: RouteWithIcon[]): NavMenuNode[] {
|
||||||
|
return routes
|
||||||
|
.filter((r) => r.title)
|
||||||
|
.map((r) => {
|
||||||
|
if (r.children) {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
children: r.children
|
||||||
|
.filter((r) => r.title)
|
||||||
|
.map((childRoute) => {
|
||||||
|
return {
|
||||||
|
...childRoute,
|
||||||
|
parentRoute: r,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
<div class="console-app__emty-registrar">
|
|
||||||
<h1>
|
|
||||||
<mat-icon class="console-app__emty-registrar-icon">block</mat-icon>
|
|
||||||
</h1>
|
|
||||||
<h1>No registrar selected</h1>
|
|
||||||
<h4 class="mat-body-2">Please select a registrar</h4>
|
|
||||||
</div>
|
|
|
@ -1,37 +0,0 @@
|
||||||
// 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, effect } from '@angular/core';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { RegistrarService } from './registrar.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-empty-registrar',
|
|
||||||
templateUrl: './emptyRegistrar.component.html',
|
|
||||||
styleUrls: ['./emptyRegistrar.component.scss'],
|
|
||||||
})
|
|
||||||
export class EmptyRegistrar {
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
protected registrarService: RegistrarService,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
effect(() => {
|
|
||||||
if (registrarService.registrarId()) {
|
|
||||||
this.router.navigate([this.route.snapshot.paramMap.get('nextUrl')]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
// 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 {
|
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { RegistrarService } from './registrar.service';
|
|
||||||
|
|
||||||
describe('RegistrarGuard', () => {
|
|
||||||
let guard: RegistrarGuard;
|
|
||||||
let dummyRegistrarService: RegistrarService;
|
|
||||||
let routeSpy: Router;
|
|
||||||
let dummyRoute: RouterStateSnapshot;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
routeSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
|
|
||||||
dummyRegistrarService = { activeRegistrarId: '' } as RegistrarService;
|
|
||||||
dummyRoute = { url: '/value' } as RouterStateSnapshot;
|
|
||||||
|
|
||||||
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(new ActivatedRouteSnapshot(), 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(new ActivatedRouteSnapshot(), dummyRoute);
|
|
||||||
expect(res).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate to empty-registrar screen when activeRegistrarId is empty', () => {
|
|
||||||
guard = TestBed.inject(RegistrarGuard);
|
|
||||||
guard.canActivate(new ActivatedRouteSnapshot(), dummyRoute);
|
|
||||||
expect(routeSpy.navigate).toHaveBeenCalledOnceWith([
|
|
||||||
'/empty-registrar',
|
|
||||||
{ nextUrl: dummyRoute.url },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,45 +0,0 @@
|
||||||
// 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 {
|
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { RegistrarService } from './registrar.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class RegistrarGuard {
|
|
||||||
constructor(
|
|
||||||
private router: Router,
|
|
||||||
private registrarService: RegistrarService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
canActivate(
|
|
||||||
_: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot
|
|
||||||
): Promise<boolean> | boolean {
|
|
||||||
if (this.registrarService.registrarId()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.router.navigate([
|
|
||||||
`/empty-registrar`,
|
|
||||||
{ nextUrl: state.url || '' },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,44 +15,53 @@
|
||||||
import { Injectable, computed, signal } from '@angular/core';
|
import { Injectable, computed, signal } from '@angular/core';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { Observable, tap } from 'rxjs';
|
||||||
|
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { BackendService } from '../shared/services/backend.service';
|
import { BackendService } from '../shared/services/backend.service';
|
||||||
import {
|
import {
|
||||||
GlobalLoader,
|
GlobalLoader,
|
||||||
GlobalLoaderService,
|
GlobalLoaderService,
|
||||||
} from '../shared/services/globalLoader.service';
|
} from '../shared/services/globalLoader.service';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
street?: string[];
|
|
||||||
city?: string;
|
city?: string;
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
zip?: string;
|
|
||||||
state?: string;
|
state?: string;
|
||||||
|
street?: string[];
|
||||||
|
zip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Registrar {
|
export interface WhoisRegistrarFields {
|
||||||
|
ianaIdentifier?: number;
|
||||||
|
icannReferralEmail: string;
|
||||||
|
localizedAddress: Address;
|
||||||
|
registrarId: string;
|
||||||
|
url: string;
|
||||||
|
whoisServer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Registrar extends WhoisRegistrarFields {
|
||||||
allowedTlds?: string[];
|
allowedTlds?: string[];
|
||||||
billingAccountMap?: object;
|
billingAccountMap?: object;
|
||||||
driveFolderId?: string;
|
driveFolderId?: string;
|
||||||
emailAddress?: string;
|
emailAddress?: string;
|
||||||
faxNumber?: string;
|
faxNumber?: string;
|
||||||
ianaIdentifier?: number;
|
|
||||||
icannReferralEmail?: string;
|
|
||||||
ipAddressAllowList?: string[];
|
ipAddressAllowList?: string[];
|
||||||
localizedAddress?: Address;
|
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
registrarId: string;
|
registrarId: string;
|
||||||
registrarName: string;
|
registrarName: string;
|
||||||
registryLockAllowed?: boolean;
|
registryLockAllowed?: boolean;
|
||||||
url?: string;
|
|
||||||
whoisServer?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class RegistrarService implements GlobalLoader {
|
export class RegistrarService implements GlobalLoader {
|
||||||
registrarId = signal<string>('');
|
registrarId = signal<string>(
|
||||||
|
new URLSearchParams(document.location.hash.split('?')[1]).get(
|
||||||
|
'registrarId'
|
||||||
|
) || ''
|
||||||
|
);
|
||||||
registrars = signal<Registrar[]>([]);
|
registrars = signal<Registrar[]>([]);
|
||||||
registrar = computed<Registrar | undefined>(() =>
|
registrar = computed<Registrar | undefined>(() =>
|
||||||
this.registrars().find((r) => r.registrarId === this.registrarId())
|
this.registrars().find((r) => r.registrarId === this.registrarId())
|
||||||
|
@ -61,7 +70,8 @@ export class RegistrarService implements GlobalLoader {
|
||||||
constructor(
|
constructor(
|
||||||
private backend: BackendService,
|
private backend: BackendService,
|
||||||
private globalLoader: GlobalLoaderService,
|
private globalLoader: GlobalLoaderService,
|
||||||
private _snackBar: MatSnackBar
|
private _snackBar: MatSnackBar,
|
||||||
|
private router: Router
|
||||||
) {
|
) {
|
||||||
this.loadRegistrars().subscribe((r) => {
|
this.loadRegistrars().subscribe((r) => {
|
||||||
this.globalLoader.stopGlobalLoader(this);
|
this.globalLoader.stopGlobalLoader(this);
|
||||||
|
@ -70,7 +80,14 @@ export class RegistrarService implements GlobalLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSelectedRegistrar(registrarId: string) {
|
public updateSelectedRegistrar(registrarId: string) {
|
||||||
this.registrarId.set(registrarId);
|
if (registrarId !== this.registrarId()) {
|
||||||
|
this.registrarId.set(registrarId);
|
||||||
|
// add registrarId to url query params, so that we can pick it up after page refresh
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { registrarId },
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadRegistrars(): Observable<Registrar[]> {
|
public loadRegistrars(): Observable<Registrar[]> {
|
||||||
|
@ -83,6 +100,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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadingTimeout() {
|
loadingTimeout() {
|
||||||
this._snackBar.open('Timeout loading registrars');
|
this._snackBar.open('Timeout loading registrars');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,99 @@
|
||||||
<div class="registrarDetails" *ngIf="registrarInEdit">
|
<div class="console-app__registrar-view">
|
||||||
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
|
<h1 class="mat-headline-4">Registrars</h1>
|
||||||
<div mat-dialog-content>
|
<mat-divider></mat-divider>
|
||||||
|
<div class="console-app__registrar-view-content">
|
||||||
|
<div class="console-app__registrar-view-controls">
|
||||||
|
<button mat-icon-button aria-label="Back to registrars list" backButton>
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
@if(!inEdit && !registrarNotFound) {
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
aria-label="Edit Registrar"
|
||||||
|
(click)="inEdit = true"
|
||||||
|
>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button aria-label="Delete Registrar">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(registrarNotFound) {
|
||||||
|
<h1>Registrar not found</h1>
|
||||||
|
} @else {
|
||||||
|
<h1>{{ registrarInEdit.registrarId }}</h1>
|
||||||
|
<h2 *ngIf="registrarInEdit.registrarName !== registrarInEdit.registrarId">
|
||||||
|
{{ registrarInEdit.registrarName }}
|
||||||
|
</h2>
|
||||||
|
@if(inEdit) {
|
||||||
<form (ngSubmit)="saveAndClose()">
|
<form (ngSubmit)="saveAndClose()">
|
||||||
<mat-form-field class="registrarDetails__input">
|
<div>
|
||||||
<mat-label>Registry Lock:</mat-label>
|
<mat-form-field appearance="outline">
|
||||||
<mat-select
|
<mat-label>Registry Lock:</mat-label>
|
||||||
[(ngModel)]="registrarInEdit.registryLockAllowed"
|
<mat-select
|
||||||
name="registryLockAllowed"
|
[(ngModel)]="registrarInEdit.registryLockAllowed"
|
||||||
>
|
name="registryLockAllowed"
|
||||||
<mat-option [value]="true">True</mat-option>
|
|
||||||
<mat-option [value]="false">False</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field class="registrarDetails__input">
|
|
||||||
<mat-label>Onboarded TLDs: </mat-label>
|
|
||||||
<mat-chip-grid #chipGrid aria-label="Enter TLD">
|
|
||||||
<mat-chip-row
|
|
||||||
*ngFor="let tld of registrarInEdit.allowedTlds"
|
|
||||||
(removed)="removeTLD(tld)"
|
|
||||||
>
|
>
|
||||||
{{ tld }}
|
<mat-option [value]="true">True</mat-option>
|
||||||
<button matChipRemove aria-label="'remove ' + tld">
|
<mat-option [value]="false">False</mat-option>
|
||||||
<mat-icon>cancel</mat-icon>
|
</mat-select>
|
||||||
</button>
|
</mat-form-field>
|
||||||
</mat-chip-row>
|
</div>
|
||||||
</mat-chip-grid>
|
<div>
|
||||||
<input
|
<mat-form-field appearance="outline">
|
||||||
placeholder="New tld..."
|
<mat-label>Onboarded TLDs: </mat-label>
|
||||||
[matChipInputFor]="chipGrid"
|
<mat-chip-grid #chipGrid aria-label="Enter TLD">
|
||||||
(matChipInputTokenEnd)="addTLD($event)"
|
<mat-chip-row
|
||||||
/>
|
*ngFor="let tld of registrarInEdit.allowedTlds"
|
||||||
</mat-form-field>
|
(removed)="removeTLD(tld)"
|
||||||
<mat-dialog-actions>
|
>
|
||||||
<button mat-button (click)="this.params?.close()">Cancel</button>
|
{{ tld }}
|
||||||
<button type="submit" mat-button color="primary">Save</button>
|
<button matChipRemove aria-label="'remove ' + tld">
|
||||||
</mat-dialog-actions>
|
<mat-icon>cancel</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-chip-row>
|
||||||
|
</mat-chip-grid>
|
||||||
|
<input
|
||||||
|
placeholder="New tld..."
|
||||||
|
[matChipInputFor]="chipGrid"
|
||||||
|
(matChipInputTokenEnd)="addTLD($event)"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
aria-label="Edit Registrar"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
} @else {
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-list role="list">
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<h2>Registrar details</h2>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
@for (column of columns; track column.columnDef) {
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">{{ column.header }} </span>
|
||||||
|
<span
|
||||||
|
class="console-app__list-value"
|
||||||
|
[innerHTML]="column.cell(registrarInEdit).replace('<br/>', ' ')"
|
||||||
|
></span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
}
|
||||||
|
</mat-list>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
} }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
.registrarDetails {
|
.console-app {
|
||||||
min-width: 30vw;
|
&__registrar-view {
|
||||||
|
&-controls {
|
||||||
&__input {
|
display: flex;
|
||||||
display: block;
|
align-items: center;
|
||||||
margin-top: 0.5rem;
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__registrar-view-content {
|
||||||
|
max-width: 616px;
|
||||||
|
mat-divider:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
mat-form-field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,38 +12,51 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Registrar, RegistrarService } from './registrar.service';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { MatChipInputEvent } from '@angular/material/chips';
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
type RegistrarDetailsParams = {
|
import { Subscription } from 'rxjs';
|
||||||
close: Function;
|
import { Registrar, RegistrarService } from './registrar.service';
|
||||||
data: {
|
import { RegistrarComponent, columns } from './registrarsTable.component';
|
||||||
registrar: Registrar;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-registrar-details',
|
selector: 'app-registrar-details',
|
||||||
templateUrl: './registrarDetails.component.html',
|
templateUrl: './registrarDetails.component.html',
|
||||||
styleUrls: ['./registrarDetails.component.scss'],
|
styleUrls: ['./registrarDetails.component.scss'],
|
||||||
})
|
})
|
||||||
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
|
export class RegistrarDetailsComponent implements OnInit {
|
||||||
|
public static PATH = 'registrars/:id';
|
||||||
|
inEdit: boolean = false;
|
||||||
registrarInEdit!: Registrar;
|
registrarInEdit!: Registrar;
|
||||||
params?: RegistrarDetailsParams;
|
registrarNotFound: boolean = false;
|
||||||
|
columns = columns.filter((c) => !c.hiddenOnDetailsCard);
|
||||||
|
private subscription!: Subscription;
|
||||||
|
|
||||||
constructor(protected registrarService: RegistrarService) {}
|
constructor(
|
||||||
|
protected registrarService: RegistrarService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private _snackBar: MatSnackBar,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
init(params: RegistrarDetailsParams) {
|
ngOnInit(): void {
|
||||||
this.params = params;
|
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
|
||||||
this.registrarInEdit = JSON.parse(
|
this.registrarInEdit = structuredClone(
|
||||||
JSON.stringify(this.params.data.registrar)
|
this.registrarService
|
||||||
);
|
.registrars()
|
||||||
}
|
.filter((r) => r.registrarId === params.get('id'))[0]
|
||||||
|
);
|
||||||
saveAndClose() {
|
if (!this.registrarInEdit) {
|
||||||
this.params?.close();
|
this._snackBar.open(
|
||||||
|
`Registrar with id ${params.get('id')} is not available`
|
||||||
|
);
|
||||||
|
this.registrarNotFound = true;
|
||||||
|
} else {
|
||||||
|
this.registrarNotFound = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addTLD(e: MatChipInputEvent) {
|
addTLD(e: MatChipInputEvent) {
|
||||||
|
@ -58,4 +71,22 @@ export class RegistrarDetailsComponent implements DialogBottomSheetContent {
|
||||||
(v) => v != tld
|
(v) => v != tld
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveAndClose() {
|
||||||
|
this.registrarService.saveRegistrar(this.registrarInEdit).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
this.router.navigate([RegistrarComponent.PATH], {
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err: HttpErrorResponse) => {
|
||||||
|
this._snackBar.open(err.error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.inEdit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,32 @@
|
||||||
<div class="console-app__registrar">
|
<div class="console-app__registrar">
|
||||||
<button
|
<mat-form-field
|
||||||
mat-button
|
class="example-full-width"
|
||||||
[routerLink]="'/settings/registrars'"
|
class="mat-form-field-density-5"
|
||||||
routerLinkActive="active"
|
appearance="outline"
|
||||||
*ngIf="isMobile; else desktop"
|
|
||||||
>
|
>
|
||||||
{{ registrarService.registrarId() || "Select registrar" }}
|
<mat-label>Registrar</mat-label>
|
||||||
<mat-icon>open_in_new</mat-icon>
|
<input
|
||||||
</button>
|
type="text"
|
||||||
<ng-template #desktop>
|
placeholder=""
|
||||||
<mat-form-field class="mat-form-field-density-5" appearance="fill">
|
aria-label="Select Registrar"
|
||||||
<mat-label>Registrar</mat-label>
|
matInput
|
||||||
<mat-select
|
[ngModel]="registrarInput()"
|
||||||
[ngModel]="registrarService.registrarId()"
|
(ngModelChange)="registrarInput.set($event)"
|
||||||
(selectionChange)="
|
[ngModelOptions]="{ standalone: true }"
|
||||||
registrarService.updateSelectedRegistrar($event.value)
|
[matAutocomplete]="auto"
|
||||||
"
|
/>
|
||||||
|
<mat-autocomplete
|
||||||
|
autoActiveFirstOption
|
||||||
|
#auto="matAutocomplete"
|
||||||
|
(optionSelected)="onSelect($event.option.value)"
|
||||||
|
>
|
||||||
|
@for (registrarId of filteredOptions; track registrarId) {
|
||||||
|
<mat-option
|
||||||
|
[value]="registrarId"
|
||||||
|
selected="registrarId === registrarService.registrarId()"
|
||||||
|
>{{ registrarId }}</mat-option
|
||||||
>
|
>
|
||||||
<mat-option
|
}
|
||||||
*ngFor="let registrar of registrarService.registrars()"
|
</mat-autocomplete>
|
||||||
[value]="registrar.registrarId"
|
</mat-form-field>
|
||||||
>
|
|
||||||
{{ registrar.registrarId }}
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,16 +3,5 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
// Fix for angular v15 label issue
|
|
||||||
// https://github.com/angular/components/issues/26579
|
|
||||||
.mat-mdc-floating-label {
|
|
||||||
display: inline !important;
|
|
||||||
}
|
|
||||||
.mat-mdc-select-arrow-wrapper {
|
|
||||||
transform: translateY(0) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__title {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,35 +12,37 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, computed, effect, signal } from '@angular/core';
|
||||||
import { RegistrarService } from './registrar.service';
|
import { RegistrarService } from './registrar.service';
|
||||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
||||||
import { distinctUntilChanged } from 'rxjs';
|
|
||||||
|
|
||||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-registrar-selector',
|
selector: 'app-registrar-selector',
|
||||||
templateUrl: './registrarSelector.component.html',
|
templateUrl: './registrarSelector.component.html',
|
||||||
styleUrls: ['./registrarSelector.component.scss'],
|
styleUrls: ['./registrarSelector.component.scss'],
|
||||||
})
|
})
|
||||||
export class RegistrarSelectorComponent implements OnInit {
|
export class RegistrarSelectorComponent {
|
||||||
protected isMobile: boolean = false;
|
registrarInput = signal<string>(this.registrarService.registrarId());
|
||||||
|
filteredOptions?: string[];
|
||||||
|
allRegistrarIds = computed(() =>
|
||||||
|
this.registrarService.registrars().map((r) => r.registrarId)
|
||||||
|
);
|
||||||
|
|
||||||
readonly breakpoint$ = this.breakpointObserver
|
constructor(protected registrarService: RegistrarService) {
|
||||||
.observe([MOBILE_LAYOUT_BREAKPOINT])
|
effect(() => {
|
||||||
.pipe(distinctUntilChanged());
|
const filterValue = this.registrarInput().toLowerCase();
|
||||||
|
this.filteredOptions = this.allRegistrarIds().filter((option) =>
|
||||||
constructor(
|
option.toLowerCase().includes(filterValue)
|
||||||
protected registrarService: RegistrarService,
|
);
|
||||||
protected breakpointObserver: BreakpointObserver
|
});
|
||||||
) {}
|
this.onSelect(registrarService.registrarId());
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.breakpoint$.subscribe(() => this.breakpointChanged());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private breakpointChanged() {
|
onSelect(registrarId: string) {
|
||||||
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
|
this.registrarService.updateSelectedRegistrar(registrarId);
|
||||||
|
|
||||||
|
// We reset the list of options after selection, so that user doesn't have to clear it out
|
||||||
|
setTimeout(() => {
|
||||||
|
this.filteredOptions = this.allRegistrarIds();
|
||||||
|
}, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<div class="console-app__registrars">
|
<div class="console-app__registrars">
|
||||||
|
<h1 class="mat-headline-4">Registrars</h1>
|
||||||
<mat-form-field class="console-app__registrars-filter">
|
<mat-form-field class="console-app__registrars-filter">
|
||||||
<mat-label>Search</mat-label>
|
<mat-label>Search</mat-label>
|
||||||
<input
|
<input
|
||||||
|
@ -11,24 +12,10 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-table
|
<mat-table
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
class="mat-elevation-z8"
|
class="mat-elevation-z0"
|
||||||
class="console-app__registrars-table"
|
class="console-app__registrars-table"
|
||||||
matSort
|
matSort
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="edit">
|
|
||||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
color="primary"
|
|
||||||
aria-label="Edit registrar"
|
|
||||||
(click)="openDetails($event, row)"
|
|
||||||
>
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngFor="let column of columns"
|
*ngFor="let column of columns"
|
||||||
[matColumnDef]="column.columnDef"
|
[matColumnDef]="column.columnDef"
|
||||||
|
@ -39,16 +26,13 @@
|
||||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<mat-row
|
<mat-row
|
||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
(click)="registrarService.updateSelectedRegistrar(row.registrarId)"
|
(click)="openDetails(row.registrarId)"
|
||||||
></mat-row>
|
></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<mat-paginator
|
<mat-paginator
|
||||||
class="mat-elevation-z8"
|
class="mat-elevation-z0"
|
||||||
[pageSizeOptions]="[5, 10, 20]"
|
[pageSizeOptions]="[5, 10, 20]"
|
||||||
showFirstLastButtons
|
showFirstLastButtons
|
||||||
></mat-paginator>
|
></mat-paginator>
|
||||||
<app-dialog-bottom-sheet-wrapper
|
|
||||||
#registrarDetailsView
|
|
||||||
></app-dialog-bottom-sheet-wrapper>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
$min-width: 756px;
|
$min-width: 756px;
|
||||||
|
|
||||||
&__registrars {
|
&__registrars {
|
||||||
margin-top: 1.5rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,12 +13,60 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
|
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||||
import { Registrar, RegistrarService } from './registrar.service';
|
|
||||||
import { MatPaginator } from '@angular/material/paginator';
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { RegistrarDetailsComponent } from './registrarDetails.component';
|
import { Router } from '@angular/router';
|
||||||
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
|
import { Registrar, RegistrarService } from './registrar.service';
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
columnDef: 'registrarId',
|
||||||
|
header: 'Registrar Id',
|
||||||
|
cell: (record: Registrar) => `${record.registrarId || ''}`,
|
||||||
|
hiddenOnDetailsCard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'registrarName',
|
||||||
|
header: 'Name',
|
||||||
|
cell: (record: Registrar) => `${record.registrarName || ''}`,
|
||||||
|
hiddenOnDetailsCard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'allowedTlds',
|
||||||
|
header: 'TLDs',
|
||||||
|
cell: (record: Registrar) => `${(record.allowedTlds || []).join(', ')}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'emailAddress',
|
||||||
|
header: 'Username',
|
||||||
|
cell: (record: Registrar) => `${record.emailAddress || ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'ianaIdentifier',
|
||||||
|
header: 'IANA ID',
|
||||||
|
cell: (record: Registrar) => `${record.ianaIdentifier || ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'billingAccountMap',
|
||||||
|
header: 'Billing Accounts',
|
||||||
|
cell: (record: Registrar) =>
|
||||||
|
// @ts-ignore - completely legit line, but TS keeps complaining
|
||||||
|
`${Object.entries(record.billingAccountMap).reduce((acc, [key, val]) => {
|
||||||
|
return `${acc}${key}=${val}<br/>`;
|
||||||
|
}, '')}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'registryLockAllowed',
|
||||||
|
header: 'Registry Lock',
|
||||||
|
cell: (record: Registrar) => `${record.registryLockAllowed}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'driveId',
|
||||||
|
header: 'Drive ID',
|
||||||
|
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-registrar',
|
selector: 'app-registrar',
|
||||||
|
@ -29,63 +77,17 @@ import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet
|
||||||
export class RegistrarComponent {
|
export class RegistrarComponent {
|
||||||
public static PATH = 'registrars';
|
public static PATH = 'registrars';
|
||||||
dataSource: MatTableDataSource<Registrar>;
|
dataSource: MatTableDataSource<Registrar>;
|
||||||
columns = [
|
columns = columns;
|
||||||
{
|
|
||||||
columnDef: 'registrarId',
|
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||||
header: 'Registrar Id',
|
|
||||||
cell: (record: Registrar) => `${record.registrarId || ''}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'registrarName',
|
|
||||||
header: 'Name',
|
|
||||||
cell: (record: Registrar) => `${record.registrarName || ''}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'allowedTlds',
|
|
||||||
header: 'TLDs',
|
|
||||||
cell: (record: Registrar) => `${(record.allowedTlds || []).join(', ')}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'emailAddress',
|
|
||||||
header: 'Username',
|
|
||||||
cell: (record: Registrar) => `${record.emailAddress || ''}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'ianaIdentifier',
|
|
||||||
header: 'IANA ID',
|
|
||||||
cell: (record: Registrar) => `${record.ianaIdentifier || ''}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'billingAccountMap',
|
|
||||||
header: 'Billing Accounts',
|
|
||||||
cell: (record: Registrar) =>
|
|
||||||
// @ts-ignore - completely legit line, but TS keeps complaining
|
|
||||||
`${Object.entries(record.billingAccountMap).reduce(
|
|
||||||
(acc, [key, val]) => {
|
|
||||||
return `${acc}${key}=${val}<br/>`;
|
|
||||||
},
|
|
||||||
''
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'registryLockAllowed',
|
|
||||||
header: 'Registry Lock',
|
|
||||||
cell: (record: Registrar) => `${record.registryLockAllowed}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnDef: 'driveId',
|
|
||||||
header: 'Drive ID',
|
|
||||||
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
displayedColumns = ['edit'].concat(this.columns.map((c) => c.columnDef));
|
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||||
@ViewChild(MatSort) sort!: MatSort;
|
@ViewChild(MatSort) sort!: MatSort;
|
||||||
@ViewChild('registrarDetailsView')
|
|
||||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
|
||||||
|
|
||||||
constructor(protected registrarService: RegistrarService) {
|
constructor(
|
||||||
|
protected registrarService: RegistrarService,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
this.dataSource = new MatTableDataSource<Registrar>(
|
this.dataSource = new MatTableDataSource<Registrar>(
|
||||||
registrarService.registrars()
|
registrarService.registrars()
|
||||||
);
|
);
|
||||||
|
@ -96,16 +98,15 @@ export class RegistrarComponent {
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
openDetails(event: MouseEvent, registrar: Registrar) {
|
openDetails(registrarId: string) {
|
||||||
event.stopPropagation();
|
this.router.navigate(['registrars/', registrarId], {
|
||||||
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
|
queryParamsHandling: 'merge',
|
||||||
RegistrarDetailsComponent,
|
});
|
||||||
{ registrar }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFilter(event: Event) {
|
applyFilter(event: Event) {
|
||||||
const filterValue = (event.target as HTMLInputElement).value;
|
const filterValue = (event.target as HTMLInputElement).value;
|
||||||
|
// TODO: consider filteing out only by registrar name
|
||||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
console-webapp/src/app/resources/resources.component.html
Normal file
15
console-webapp/src/app/resources/resources.component.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<h1 class="mat-headline-4">Resource</h1>
|
||||||
|
<div class="console-app__resources">
|
||||||
|
<div>
|
||||||
|
<div class="console-app__resources-subhead">Technical resources</div>
|
||||||
|
<a
|
||||||
|
class="text-l"
|
||||||
|
href="{{ userDataService.userData.technicalDocsUrl }}"
|
||||||
|
target="_blank"
|
||||||
|
>View on Google Drive</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="./assets/resources.png" />
|
||||||
|
</div>
|
||||||
|
</div>
|
22
console-webapp/src/app/resources/resources.component.scss
Normal file
22
console-webapp/src/app/resources/resources.component.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
.console-app__resources {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap-reverse;
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 300px;
|
||||||
|
min-width: 200px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&-subhead {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,12 +13,14 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { UserDataService } from 'src/app/shared/services/userData.service';
|
import { UserDataService } from '../shared/services/userData.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[app-resources-widget]',
|
selector: 'app-resources',
|
||||||
templateUrl: './resourcesWidget.component.html',
|
templateUrl: './resources.component.html',
|
||||||
|
styleUrls: ['./resources.component.scss'],
|
||||||
})
|
})
|
||||||
export class ResourcesWidgetComponent {
|
export class ResourcesComponent {
|
||||||
constructor(public userDataService: UserDataService) {}
|
public static PATH = 'resources';
|
||||||
|
constructor(protected userDataService: UserDataService) {}
|
||||||
}
|
}
|
|
@ -1,48 +1,40 @@
|
||||||
@if (loading) {
|
@if(contactService.isContactDetailsView || contactService.isContactNewView) {
|
||||||
<div class="contact__loading">
|
<app-contact-details></app-contact-details>
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
||||||
</div>
|
|
||||||
} @else {
|
} @else {
|
||||||
<div>
|
<div class="console-app__contacts-controls">
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
(click)="openNewContact()"
|
||||||
|
aria-label="Add new contact"
|
||||||
|
>
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
Add new contact
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="console-app__contacts">
|
||||||
@if (contactService.contacts().length === 0) {
|
@if (contactService.contacts().length === 0) {
|
||||||
<div class="contact__empty-contacts">
|
<div class="console-app__empty-contacts">
|
||||||
<mat-icon class="contact__empty-contacts-icon secondary-text"
|
<mat-icon class="console-app__empty-contacts-icon secondary-text"
|
||||||
>apps_outage</mat-icon
|
>apps_outage</mat-icon
|
||||||
>
|
>
|
||||||
<h1>No contacts found</h1>
|
<h1>No contacts found</h1>
|
||||||
</div>
|
</div>
|
||||||
} @else { @for (group of groupedContacts(); track group.emailAddress) {
|
} @else {
|
||||||
<div class="contact__cards-wrapper">
|
<mat-table [dataSource]="dataSource" class="mat-elevation-z0">
|
||||||
<h3>{{ group.label }}s</h3>
|
<ng-container
|
||||||
<mat-divider></mat-divider>
|
*ngFor="let column of columns"
|
||||||
<div class="contact__cards">
|
[matColumnDef]="column.columnDef"
|
||||||
<mat-card class="contact__card" *ngFor="let contact of group.contacts">
|
>
|
||||||
<mat-card-title>{{ contact.name }}</mat-card-title>
|
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
|
||||||
<p *ngIf="contact.phoneNumber">{{ contact.phoneNumber }}</p>
|
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||||
<p *ngIf="contact.emailAddress">{{ contact.emailAddress }}</p>
|
</ng-container>
|
||||||
<mat-card-actions class="contact__card-actions">
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<button
|
<mat-row
|
||||||
mat-button
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
color="primary"
|
(click)="openDetails(row)"
|
||||||
(click)="openDetails($event, contact)"
|
></mat-row>
|
||||||
>
|
</mat-table>
|
||||||
<mat-icon>edit</mat-icon>Edit
|
}
|
||||||
</button>
|
|
||||||
<button mat-button color="accent" (click)="deleteContact(contact)">
|
|
||||||
<mat-icon>delete</mat-icon>Delete
|
|
||||||
</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} }
|
|
||||||
<div class="contact__actions">
|
|
||||||
<button mat-raised-button color="primary" (click)="openCreateNew($event)">
|
|
||||||
<mat-icon>add</mat-icon>Create a Contact
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<app-dialog-bottom-sheet-wrapper
|
|
||||||
#contactDetailsWrapper
|
|
||||||
></app-dialog-bottom-sheet-wrapper>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,30 +12,14 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
.contact {
|
.console-app {
|
||||||
&__cards-wrapper {
|
&__contacts {
|
||||||
margin-top: 22px;
|
margin-top: -10px;
|
||||||
}
|
width: 100%;
|
||||||
&__card {
|
overflow: auto;
|
||||||
width: 400px;
|
mat-table {
|
||||||
padding: 15px;
|
min-width: 800px;
|
||||||
}
|
}
|
||||||
&__card-actions {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
&__loading {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
&__cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
&__actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
}
|
||||||
&__empty-contacts {
|
&__empty-contacts {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,22 +33,14 @@
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
&__contacts-controls {
|
||||||
.contact-details {
|
position: absolute;
|
||||||
&__input {
|
right: 20px;
|
||||||
width: 100%;
|
top: 5px;
|
||||||
}
|
}
|
||||||
&__group {
|
.contact__name-column-title {
|
||||||
margin: 20px 0;
|
color: #5f6368;
|
||||||
display: flex;
|
font-weight: 500;
|
||||||
align-items: baseline;
|
padding: 10px 0;
|
||||||
}
|
|
||||||
&__group-content {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
max-width: 450px;
|
|
||||||
* {
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,101 +12,16 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ViewChild, computed } from '@angular/core';
|
import { Component, effect } from '@angular/core';
|
||||||
import { Contact, ContactService } from './contact.service';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { take } from 'rxjs';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||||
import {
|
import {
|
||||||
DialogBottomSheetContent,
|
Contact,
|
||||||
DialogBottomSheetWrapper,
|
ContactService,
|
||||||
} from 'src/app/shared/components/dialogBottomSheet.component';
|
ViewReadyContact,
|
||||||
|
contactTypeToViewReadyContact,
|
||||||
enum Operations {
|
} from './contact.service';
|
||||||
DELETE,
|
|
||||||
ADD,
|
|
||||||
UPDATE,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GroupedContacts {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
contacts: Array<Contact>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContactDetailsParams = {
|
|
||||||
close: Function;
|
|
||||||
data: {
|
|
||||||
contact: Contact;
|
|
||||||
operation: Operations;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const contactTypes: Array<GroupedContacts> = [
|
|
||||||
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
|
|
||||||
{ value: 'ABUSE', label: 'Abuse contact', contacts: [] },
|
|
||||||
{ value: 'BILLING', label: 'Billing contact', contacts: [] },
|
|
||||||
{ value: 'LEGAL', label: 'Legal contact', contacts: [] },
|
|
||||||
{ value: 'MARKETING', label: 'Marketing contact', contacts: [] },
|
|
||||||
{ value: 'TECH', label: 'Technical contact', contacts: [] },
|
|
||||||
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
|
|
||||||
];
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-contact-details-dialog',
|
|
||||||
templateUrl: 'contactDetails.component.html',
|
|
||||||
styleUrls: ['./contact.component.scss'],
|
|
||||||
})
|
|
||||||
export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
|
||||||
contact?: Contact;
|
|
||||||
contactTypes = contactTypes;
|
|
||||||
contactIndex?: number;
|
|
||||||
|
|
||||||
params?: ContactDetailsParams;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public contactService: ContactService,
|
|
||||||
private _snackBar: MatSnackBar
|
|
||||||
) {}
|
|
||||||
|
|
||||||
init(params: ContactDetailsParams) {
|
|
||||||
this.params = params;
|
|
||||||
this.contactIndex = this.contactService
|
|
||||||
.contacts()
|
|
||||||
.findIndex((c) => c === params.data.contact);
|
|
||||||
this.contact = JSON.parse(JSON.stringify(params.data.contact));
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.params?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAndClose(e: SubmitEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!this.contact || this.contactIndex === undefined) return;
|
|
||||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const operation = this.params?.data.operation;
|
|
||||||
let operationObservable;
|
|
||||||
if (operation === Operations.ADD) {
|
|
||||||
operationObservable = this.contactService.addContact(this.contact);
|
|
||||||
} else if (operation === Operations.UPDATE) {
|
|
||||||
operationObservable = this.contactService.updateContact(
|
|
||||||
this.contactIndex,
|
|
||||||
this.contact
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw 'Unknown operation type';
|
|
||||||
}
|
|
||||||
|
|
||||||
operationObservable.subscribe({
|
|
||||||
complete: () => this.close(),
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this._snackBar.open(err.error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-contact',
|
selector: 'app-contact',
|
||||||
|
@ -115,61 +30,67 @@ export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
||||||
})
|
})
|
||||||
export default class ContactComponent {
|
export default class ContactComponent {
|
||||||
public static PATH = 'contact';
|
public static PATH = 'contact';
|
||||||
public groupedContacts = computed(() => {
|
|
||||||
return this.contactService.contacts().reduce((acc, contact) => {
|
|
||||||
contact.types.forEach((contactType) => {
|
|
||||||
acc
|
|
||||||
.find((group: GroupedContacts) => group.value === contactType)
|
|
||||||
?.contacts.push(contact);
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, JSON.parse(JSON.stringify(contactTypes)));
|
|
||||||
});
|
|
||||||
|
|
||||||
@ViewChild('contactDetailsWrapper')
|
dataSource: MatTableDataSource<ViewReadyContact> =
|
||||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
new MatTableDataSource<ViewReadyContact>([]);
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
columnDef: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
cell: (contact: ViewReadyContact) => `
|
||||||
|
<div class="contact__name-column">
|
||||||
|
<div class="contact__name-column-title">${contact.name}</div>
|
||||||
|
<div class="contact__name-column-roles">${contact.userFriendlyTypes.join(
|
||||||
|
' • '
|
||||||
|
)}</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'emailAddress',
|
||||||
|
header: 'Email',
|
||||||
|
cell: (contact: ViewReadyContact) => `${contact.emailAddress || ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'phoneNumber',
|
||||||
|
header: 'Phone',
|
||||||
|
cell: (contact: ViewReadyContact) => `${contact.phoneNumber || ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'faxNumber',
|
||||||
|
header: 'Fax',
|
||||||
|
cell: (contact: ViewReadyContact) => `${contact.faxNumber || ''}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||||
|
|
||||||
loading: boolean = false;
|
|
||||||
constructor(
|
constructor(
|
||||||
public contactService: ContactService,
|
public contactService: ContactService,
|
||||||
private _snackBar: MatSnackBar
|
private registrarService: RegistrarService
|
||||||
) {
|
) {
|
||||||
// TODO: Refactor to registrarId service
|
effect(() => {
|
||||||
this.loading = true;
|
if (this.registrarService.registrarId()) {
|
||||||
this.contactService.fetchContacts().subscribe(() => {
|
this.contactService.isContactDetailsView = false;
|
||||||
this.loading = false;
|
this.contactService.isContactNewView = false;
|
||||||
|
this.contactService
|
||||||
|
.fetchContacts()
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((contacts) => {
|
||||||
|
this.dataSource = new MatTableDataSource<ViewReadyContact>(
|
||||||
|
contacts.map(contactTypeToViewReadyContact)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteContact(contact: Contact) {
|
openDetails(contact: Contact) {
|
||||||
if (confirm(`Please confirm contact ${contact.name} delete`)) {
|
this.contactService.setEditableContact(contact);
|
||||||
this.contactService.deleteContact(contact).subscribe({
|
this.contactService.isContactDetailsView = true;
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this._snackBar.open(err.error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateNew(e: MouseEvent) {
|
openNewContact() {
|
||||||
const newContact: Contact = {
|
this.contactService.setEditableContact();
|
||||||
name: '',
|
this.contactService.isContactNewView = true;
|
||||||
phoneNumber: '',
|
|
||||||
emailAddress: '',
|
|
||||||
types: [contactTypes[0].value],
|
|
||||||
};
|
|
||||||
this.openDetails(e, newContact, Operations.ADD);
|
|
||||||
}
|
|
||||||
|
|
||||||
openDetails(
|
|
||||||
e: MouseEvent,
|
|
||||||
contact: Contact,
|
|
||||||
operation: Operations = Operations.UPDATE
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.detailsComponentWrapper.open<ContactDetailsDialogComponent>(
|
|
||||||
ContactDetailsDialogComponent,
|
|
||||||
{ contact, operation }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,65 +13,112 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { Observable, switchMap, tap } from 'rxjs';
|
||||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
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 type contactType =
|
||||||
|
| 'ADMIN'
|
||||||
|
| 'ABUSE'
|
||||||
|
| 'BILLING'
|
||||||
|
| 'LEGAL'
|
||||||
|
| 'MARKETING'
|
||||||
|
| 'TECH'
|
||||||
|
| 'WHOIS';
|
||||||
|
|
||||||
|
type contactTypesToUserFriendlyTypes = { [type in contactType]: string };
|
||||||
|
|
||||||
|
export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
|
||||||
|
ADMIN: 'Primary contact',
|
||||||
|
ABUSE: 'Abuse contact',
|
||||||
|
BILLING: 'Billing contact',
|
||||||
|
LEGAL: 'Legal contact',
|
||||||
|
MARKETING: 'Marketing contact',
|
||||||
|
TECH: 'Technical contact',
|
||||||
|
WHOIS: 'WHOIS-Inquiry contact',
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];
|
||||||
|
|
||||||
export interface Contact {
|
export interface Contact {
|
||||||
name: string;
|
name: string;
|
||||||
phoneNumber: string;
|
phoneNumber?: string;
|
||||||
emailAddress: string;
|
emailAddress: string;
|
||||||
registrarId?: string;
|
registrarId?: string;
|
||||||
faxNumber?: string;
|
faxNumber?: string;
|
||||||
types: Array<string>;
|
types: Array<contactType>;
|
||||||
visibleInWhoisAsAdmin?: boolean;
|
visibleInWhoisAsAdmin?: boolean;
|
||||||
visibleInWhoisAsTech?: boolean;
|
visibleInWhoisAsTech?: boolean;
|
||||||
visibleInDomainWhoisAsAbuse?: boolean;
|
visibleInDomainWhoisAsAbuse?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewReadyContact extends Contact {
|
||||||
|
userFriendlyTypes: Array<UserFriendlyType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contactTypeToViewReadyContact(c: Contact): ViewReadyContact {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
userFriendlyTypes: c.types?.map((cType) => contactTypeToTextMap[cType]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ContactService {
|
export class ContactService {
|
||||||
contacts = signal<Contact[]>([]);
|
contacts = signal<ViewReadyContact[]>([]);
|
||||||
|
contactInEdit!: ViewReadyContact;
|
||||||
|
isContactDetailsView: boolean = false;
|
||||||
|
isContactNewView: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private backend: BackendService,
|
private backend: BackendService,
|
||||||
private registrarService: RegistrarService
|
private registrarService: RegistrarService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// TODO: Come up with a better handling for registrarId
|
setEditableContact(contact?: Contact) {
|
||||||
|
this.contactInEdit = contactTypeToViewReadyContact(
|
||||||
|
contact
|
||||||
|
? contact
|
||||||
|
: {
|
||||||
|
emailAddress: '',
|
||||||
|
name: '',
|
||||||
|
types: ['ADMIN'],
|
||||||
|
faxNumber: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
registrarId: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fetchContacts(): Observable<Contact[]> {
|
fetchContacts(): Observable<Contact[]> {
|
||||||
return this.backend.getContacts(this.registrarService.registrarId()).pipe(
|
return this.backend.getContacts(this.registrarService.registrarId()).pipe(
|
||||||
tap((contacts = []) => {
|
tap((contacts) => {
|
||||||
this.contacts.set(contacts);
|
this.contacts.set(contacts.map(contactTypeToViewReadyContact));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveContacts(contacts: Contact[]): Observable<Contact[]> {
|
saveContacts(contacts: ViewReadyContact[]): Observable<Contact[]> {
|
||||||
return this.backend
|
return this.backend
|
||||||
.postContacts(this.registrarService.registrarId(), contacts)
|
.postContacts(this.registrarService.registrarId(), contacts)
|
||||||
.pipe(
|
.pipe(switchMap((_) => this.fetchContacts()));
|
||||||
tap((_) => {
|
|
||||||
this.contacts.set(contacts);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContact(index: number, contact: Contact) {
|
updateContact(index: number, contact: ViewReadyContact) {
|
||||||
const newContacts = this.contacts().map((c, i) =>
|
const newContacts = this.contacts().map((c, i) =>
|
||||||
i === index ? contact : c
|
i === index ? contact : c
|
||||||
);
|
);
|
||||||
return this.saveContacts(newContacts);
|
return this.saveContacts(newContacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
addContact(contact: Contact) {
|
addContact(contact: ViewReadyContact) {
|
||||||
const newContacts = this.contacts().concat([contact]);
|
const newContacts = this.contacts().concat([contact]);
|
||||||
return this.saveContacts(newContacts);
|
return this.saveContacts(newContacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteContact(contact: Contact) {
|
deleteContact(contact: ViewReadyContact) {
|
||||||
const newContacts = this.contacts().filter((c) => c !== contact);
|
const newContacts = this.contacts().filter((c) => c !== contact);
|
||||||
return this.saveContacts(newContacts);
|
return this.saveContacts(newContacts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,104 +1,201 @@
|
||||||
<h3 mat-dialog-title>Contact details</h3>
|
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
|
||||||
<div mat-dialog-content *ngIf="contact">
|
<div class="console-app__contact-controls">
|
||||||
<form (ngSubmit)="saveAndClose($event)">
|
<button
|
||||||
<p>
|
mat-icon-button
|
||||||
<mat-form-field class="contact-details__input">
|
aria-label="Back to contacts list"
|
||||||
|
(click)="goBack()"
|
||||||
|
>
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
@if(!isEditing && !contactService.isContactNewView) {
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
aria-label="Edit Contact"
|
||||||
|
(click)="isEditing = true"
|
||||||
|
>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button aria-label="Delete Contact">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(isEditing || contactService.isContactNewView) {
|
||||||
|
<h1>Contact Details</h1>
|
||||||
|
<form (ngSubmit)="save($event)">
|
||||||
|
<section>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Name: </mat-label>
|
<mat-label>Name: </mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
[required]="true"
|
[required]="true"
|
||||||
[(ngModel)]="contact.name"
|
[(ngModel)]="contactService.contactInEdit.name"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[ngModelOptions]="{ standalone: true }"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<mat-form-field appearance="outline">
|
||||||
<mat-form-field class="contact-details__input">
|
|
||||||
<mat-label>Primary account email: </mat-label>
|
<mat-label>Primary account email: </mat-label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
matInput
|
matInput
|
||||||
[email]="true"
|
[email]="true"
|
||||||
[required]="true"
|
[required]="true"
|
||||||
[(ngModel)]="contact.emailAddress"
|
[(ngModel)]="contactService.contactInEdit.emailAddress"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[ngModelOptions]="{ standalone: true }"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<mat-form-field appearance="outline">
|
||||||
<mat-form-field class="contact-details__input">
|
|
||||||
<mat-label>Phone: </mat-label>
|
<mat-label>Phone: </mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
[(ngModel)]="contact.phoneNumber"
|
[(ngModel)]="contactService.contactInEdit.phoneNumber"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
placeholder="+0.0000000000"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<mat-form-field appearance="outline">
|
||||||
<mat-form-field class="contact-details__input">
|
|
||||||
<mat-label>Fax: </mat-label>
|
<mat-label>Fax: </mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
[(ngModel)]="contact.faxNumber"
|
[(ngModel)]="contactService.contactInEdit.faxNumber"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[ngModelOptions]="{ standalone: true }"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</p>
|
</section>
|
||||||
|
|
||||||
<div class="contact-details__group">
|
<section>
|
||||||
<label>Contact type:</label>
|
<h1>Contact Type</h1>
|
||||||
<div class="contact-details__group-content">
|
<p class="console-app__contact-required">
|
||||||
|
<mat-icon color="accent">error</mat-icon>Required to select at least one
|
||||||
|
</p>
|
||||||
|
<div class="">
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
*ngFor="let contactType of contactTypes"
|
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
|
||||||
[checked]="contact.types.includes(contactType.value)"
|
[checked]="checkboxIsChecked(contactType.key)"
|
||||||
(change)="
|
(change)="checkboxOnChange($event, contactType.key)"
|
||||||
$event.checked
|
[disabled]="checkboxIsDisabled(contactType.key)"
|
||||||
? contact.types.push(contactType.value)
|
|
||||||
: contact.types.splice(
|
|
||||||
contact.types.indexOf(contactType.value),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
"
|
|
||||||
[disabled]="
|
|
||||||
contact.types.length === 1 && contact.types[0] === contactType.value
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{{ contactType.label }}
|
{{ contactType.value }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<mat-checkbox
|
|
||||||
[(ngModel)]="contact.visibleInWhoisAsAdmin"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
>Show in Registrar WHOIS record as admin contact</mat-checkbox
|
|
||||||
>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<mat-checkbox
|
<h1>WHOIS Preferences</h1>
|
||||||
[(ngModel)]="contact.visibleInWhoisAsTech"
|
<div>
|
||||||
[ngModelOptions]="{ standalone: true }"
|
<mat-checkbox
|
||||||
>Show in Registrar WHOIS record as technical contact</mat-checkbox
|
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
|
||||||
>
|
[ngModelOptions]="{ standalone: true }"
|
||||||
</section>
|
>Show in Registrar WHOIS record as admin contact</mat-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section>
|
<div>
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
[(ngModel)]="contact.visibleInDomainWhoisAsAbuse"
|
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[ngModelOptions]="{ standalone: true }"
|
||||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse contact
|
>Show in Registrar WHOIS record as technical contact</mat-checkbox
|
||||||
(per CL&D requirements)</mat-checkbox
|
>
|
||||||
>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<mat-checkbox
|
||||||
|
[(ngModel)]="contactService.contactInEdit.visibleInDomainWhoisAsAbuse"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||||
|
contact (per CL&D requirements)</mat-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<mat-dialog-actions>
|
<button
|
||||||
<button mat-button (click)="close()">Cancel</button>
|
class="console-app__contact-submit"
|
||||||
<button type="submit" mat-button>Save</button>
|
mat-flat-button
|
||||||
</mat-dialog-actions>
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
} @else {
|
||||||
|
<h1>{{ contactService.contactInEdit.name }}</h1>
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-list role="list">
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<h2>Contact details</h2>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Email</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
contactService.contactInEdit.emailAddress
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Phone</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
contactService.contactInEdit.phoneNumber
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Fax</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
contactService.contactInEdit.faxNumber
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Type:</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
contactService.contactInEdit.userFriendlyTypes.join(", ")
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-list role="list">
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<h2>WHOIS Preferences</h2>
|
||||||
|
</mat-list-item>
|
||||||
|
@if(contactService.contactInEdit.visibleInWhoisAsAdmin) {
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-value"
|
||||||
|
>Show in Registrar WHOIS record as admin contact</span
|
||||||
|
>
|
||||||
|
</mat-list-item>
|
||||||
|
} @if(contactService.contactInEdit.visibleInWhoisAsTech) {
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item
|
||||||
|
role="listitem"
|
||||||
|
*ngIf="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||||
|
>
|
||||||
|
<span class="console-app__list-value"
|
||||||
|
>Show in Registrar WHOIS record as technical contact</span
|
||||||
|
>
|
||||||
|
</mat-list-item>
|
||||||
|
} @if(contactService.contactInEdit.visibleInDomainWhoisAsAbuse) {
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-value"
|
||||||
|
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||||
|
contact (per CL&D requirements)</span
|
||||||
|
>
|
||||||
|
</mat-list-item>
|
||||||
|
}
|
||||||
|
</mat-list>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
.console-app__contact {
|
||||||
|
max-width: 616px;
|
||||||
|
section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
&-required {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-submit {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-card {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 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 } from '@angular/core';
|
||||||
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { first } from 'rxjs';
|
||||||
|
import {
|
||||||
|
ContactService,
|
||||||
|
contactType,
|
||||||
|
contactTypeToTextMap,
|
||||||
|
} from './contact.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-contact-details',
|
||||||
|
templateUrl: './contactDetails.component.html',
|
||||||
|
styleUrls: ['./contactDetails.component.scss'],
|
||||||
|
})
|
||||||
|
export class ContactDetailsComponent {
|
||||||
|
protected contactTypeToTextMap = contactTypeToTextMap;
|
||||||
|
isEditing: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected contactService: ContactService,
|
||||||
|
private _snackBar: MatSnackBar
|
||||||
|
) {}
|
||||||
|
|
||||||
|
deleteContact() {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
`Please confirm deletion of contact ${this.contactService.contactInEdit.name}`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.contactService
|
||||||
|
.deleteContact(this.contactService.contactInEdit)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
error: (err: HttpErrorResponse) => {
|
||||||
|
this._snackBar.open(err.error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
if (this.isEditing) {
|
||||||
|
this.isEditing = false;
|
||||||
|
} else {
|
||||||
|
this.contactService.isContactNewView = false;
|
||||||
|
this.contactService.isContactDetailsView = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.contactService
|
||||||
|
.saveContacts([this.contactService.contactInEdit])
|
||||||
|
.subscribe({
|
||||||
|
complete: () => {
|
||||||
|
this.goBack();
|
||||||
|
},
|
||||||
|
error: (err: HttpErrorResponse) => {
|
||||||
|
this._snackBar.open(err.error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkboxIsChecked(type: string) {
|
||||||
|
return this.contactService.contactInEdit.types.includes(
|
||||||
|
type as contactType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkboxIsDisabled(type: string) {
|
||||||
|
return (
|
||||||
|
this.contactService.contactInEdit.types.length === 1 &&
|
||||||
|
this.contactService.contactInEdit.types[0] === (type as contactType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkboxOnChange(event: MatCheckboxChange, type: string) {
|
||||||
|
if (event.checked) {
|
||||||
|
this.contactService.contactInEdit.types.push(type as contactType);
|
||||||
|
} else {
|
||||||
|
this.contactService.contactInEdit.types =
|
||||||
|
this.contactService.contactInEdit.types.filter(
|
||||||
|
(t) => t != (type as contactType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,98 +1,82 @@
|
||||||
<div *ngIf="loading" class="settings-security__loading">
|
@if(securityService.isEditingSecurity) {
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
<app-security-edit></app-security-edit>
|
||||||
</div>
|
} @else {
|
||||||
<div class="settings-security" *ngIf="!loading">
|
<div class="settings-security">
|
||||||
<div class="settings-security__section">
|
<mat-card appearance="outlined">
|
||||||
<div class="settings-security__section-description">
|
<mat-card-content>
|
||||||
<h2>IP Allowlist</h2>
|
<mat-list role="list">
|
||||||
<p>
|
<!-- IP Allowlist Start -->
|
||||||
Restrict access to EPP production servers to the following IP/IPv6
|
<mat-list-item role="listitem">
|
||||||
addresses, or ranges like 1.1.1.0/24
|
<div class="settings-security__section-header">
|
||||||
</p>
|
<h2>IP Allowlist</h2>
|
||||||
</div>
|
|
||||||
<div class="settings-security__section-form">
|
|
||||||
<div
|
|
||||||
*ngIf="
|
|
||||||
dataSource.ipAddressAllowList &&
|
|
||||||
dataSource.ipAddressAllowList.length > 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
*ngFor="let item of dataSource.ipAddressAllowList; index as index"
|
|
||||||
class="settings-security__ipRecord"
|
|
||||||
>
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
class="settings-security__ip-allowlist"
|
|
||||||
[(ngModel)]="item.value"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
*ngIf="inEdit"
|
mat-flat-button
|
||||||
matSuffix
|
color="primary"
|
||||||
mat-icon-button
|
aria-label="Edit Contact"
|
||||||
aria-label="Remove"
|
(click)="editSecurity()"
|
||||||
(click)="removeIpEntry(index)"
|
|
||||||
>
|
>
|
||||||
<mat-icon>close</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</mat-form-field>
|
</div>
|
||||||
</div>
|
</mat-list-item>
|
||||||
</div>
|
<mat-list-item role="listitem" lines="3">
|
||||||
<button mat-stroked-button (click)="enableEdit(); createIpEntry()">
|
<span class="console-app__list-value"
|
||||||
Add IP
|
>Restrict access to EPP production servers to the following IP/IPv6
|
||||||
</button>
|
addresses, or ranges like 1.1.1.0/24</span
|
||||||
</div>
|
>
|
||||||
</div>
|
</mat-list-item>
|
||||||
<div class="settings-security__section">
|
<mat-divider></mat-divider>
|
||||||
<div class="settings-security__section-description">
|
@for (item of dataSource.ipAddressAllowList; track item.value) {
|
||||||
<h2>SSL Certificate</h2>
|
<mat-list-item role="listitem">
|
||||||
<p>X.509 PEM certificate for EPP production access.</p>
|
<span class="console-app__list-value">{{ item.value }}</span>
|
||||||
</div>
|
</mat-list-item>
|
||||||
<div class="settings-security__section-form">
|
<mat-divider></mat-divider>
|
||||||
<textarea
|
} @empty {
|
||||||
matInput
|
<mat-list-item role="listitem">
|
||||||
class="settings-security__clientCertificate"
|
<span class="console-app__list-value">No IP addresses on file.</span>
|
||||||
[(ngModel)]="dataSource.clientCertificate"
|
</mat-list-item>
|
||||||
[disabled]="!inEdit"
|
}
|
||||||
></textarea>
|
<mat-list-item role="listitem"></mat-list-item>
|
||||||
</div>
|
<!-- IP Allowlist End -->
|
||||||
</div>
|
|
||||||
<div class="settings-security__section">
|
<!-- SSL Certificate Start -->
|
||||||
<div class="settings-security__section-description">
|
<mat-list-item role="listitem">
|
||||||
<h2>Failover SSL Certificate</h2>
|
<h2>SSL Certificate</h2>
|
||||||
<p>X.509 PEM backup certificate for EPP Production Access.</p>
|
</mat-list-item>
|
||||||
</div>
|
<mat-list-item role="listitem">
|
||||||
<div class="settings-security__section-form">
|
<span class="console-app__list-value"
|
||||||
<textarea
|
>X.509 PEM certificate for EPP production access</span
|
||||||
matInput
|
>
|
||||||
[(ngModel)]="dataSource.failoverClientCertificate"
|
</mat-list-item>
|
||||||
[disabled]="!inEdit"
|
<mat-divider></mat-divider>
|
||||||
></textarea>
|
<mat-list-item role="listitem" lines="10">
|
||||||
</div>
|
<span class="console-app__list-value">{{
|
||||||
</div>
|
dataSource.clientCertificate || "No client certificate on file."
|
||||||
<div class="settings-security__actions">
|
}}</span>
|
||||||
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
</mat-list-item>
|
||||||
<button
|
<mat-list-item role="listitem"> </mat-list-item>
|
||||||
class="settings-security__actions-save"
|
<!-- SSL Certificate End -->
|
||||||
mat-raised-button
|
|
||||||
color="primary"
|
<!-- Failover SSL Certificate Start -->
|
||||||
(click)="save()"
|
<mat-list-item role="listitem">
|
||||||
>
|
<h2>Failover SSL Certificate</h2>
|
||||||
Save
|
</mat-list-item>
|
||||||
</button>
|
<mat-list-item role="listitem">
|
||||||
<button
|
<span class="console-app__list-value"
|
||||||
class="settings-security__actions-cancel"
|
>X.509 PEM backup certificate for EPP production access</span
|
||||||
mat-stroked-button
|
>
|
||||||
(click)="cancel()"
|
</mat-list-item>
|
||||||
>
|
<mat-divider></mat-divider>
|
||||||
Cancel
|
<mat-list-item role="listitem" lines="10">
|
||||||
</button>
|
<span class="console-app__list-value">{{
|
||||||
</ng-template>
|
dataSource.failoverClientCertificate ||
|
||||||
<ng-template #inView>
|
"No failover certificate on file."
|
||||||
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
|
}}</span>
|
||||||
</ng-template>
|
</mat-list-item>
|
||||||
</div>
|
<!-- Failover SSL Certificate End -->
|
||||||
|
</mat-list>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,41 +13,14 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
.settings-security {
|
.settings-security {
|
||||||
margin-top: 1.5rem;
|
max-width: 616px;
|
||||||
h1 {
|
mat-card {
|
||||||
margin: 0;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
&__ipRecord {
|
&__section-header {
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
&__section {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
justify-content: space-between;
|
||||||
margin-bottom: 3rem;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
width: 100%;
|
||||||
}
|
|
||||||
&__section-description {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
&__section-form {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 300px;
|
|
||||||
textarea {
|
|
||||||
min-height: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
button {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__loading {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,70 +12,35 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, effect } from '@angular/core';
|
||||||
|
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||||
import {
|
import {
|
||||||
SecurityService,
|
SecurityService,
|
||||||
SecuritySettings,
|
SecuritySettings,
|
||||||
apiToUiConverter,
|
apiToUiConverter,
|
||||||
} from './security.service';
|
} from './security.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-security',
|
selector: 'app-security',
|
||||||
templateUrl: './security.component.html',
|
templateUrl: './security.component.html',
|
||||||
styleUrls: ['./security.component.scss'],
|
styleUrls: ['./security.component.scss'],
|
||||||
providers: [SecurityService],
|
|
||||||
})
|
})
|
||||||
export default class SecurityComponent {
|
export default class SecurityComponent {
|
||||||
public static PATH = 'security';
|
public static PATH = 'security';
|
||||||
|
|
||||||
loading: boolean = false;
|
|
||||||
inEdit: boolean = false;
|
|
||||||
dataSource: SecuritySettings = {};
|
dataSource: SecuritySettings = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public securityService: SecurityService,
|
public securityService: SecurityService,
|
||||||
private _snackBar: MatSnackBar,
|
|
||||||
public registrarService: RegistrarService
|
public registrarService: RegistrarService
|
||||||
) {
|
) {
|
||||||
this.dataSource = apiToUiConverter(this.registrarService.registrar());
|
effect(() => {
|
||||||
}
|
if (this.registrarService.registrar()) {
|
||||||
|
this.dataSource = apiToUiConverter(this.registrarService.registrar());
|
||||||
enableEdit() {
|
this.securityService.isEditingSecurity = false;
|
||||||
this.inEdit = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.inEdit = false;
|
|
||||||
this.resetDataSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
createIpEntry() {
|
|
||||||
this.dataSource.ipAddressAllowList?.push({ value: '' });
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.loading = true;
|
|
||||||
this.securityService.saveChanges(this.dataSource).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
this.loading = false;
|
|
||||||
this.resetDataSource();
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this._snackBar.open(err.error);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
this.cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeIpEntry(index: number) {
|
editSecurity() {
|
||||||
this.dataSource.ipAddressAllowList =
|
this.securityService.isEditingSecurity = true;
|
||||||
this.dataSource.ipAddressAllowList?.filter((_, i) => i != index);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDataSource() {
|
|
||||||
this.dataSource = apiToUiConverter(this.registrarService.registrar());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -17,7 +17,7 @@ import { switchMap } from 'rxjs';
|
||||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
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';
|
||||||
|
|
||||||
interface ipAllowListItem {
|
export interface ipAllowListItem {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
export interface SecuritySettings {
|
export interface SecuritySettings {
|
||||||
|
@ -52,9 +52,12 @@ export function uiToApiConverter(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
export class SecurityService {
|
export class SecurityService {
|
||||||
securitySettings: SecuritySettings = {};
|
securitySettings: SecuritySettings = {};
|
||||||
|
isEditingSecurity: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private backend: BackendService,
|
private backend: BackendService,
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<div class="settings-security__edit">
|
||||||
|
<h1>IP Allowlist</h1>
|
||||||
|
<p>
|
||||||
|
Restrict access to EPP production servers to the following IP/IPv6
|
||||||
|
addresses, or ranges like 1.1.1.0/24
|
||||||
|
</p>
|
||||||
|
<form (ngSubmit)="save()">
|
||||||
|
@for (ip of dataSource.ipAddressAllowList; track ip.value) {
|
||||||
|
<div class="settings-security__edit-ip">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="ip.value"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
matSuffix
|
||||||
|
mat-icon-button
|
||||||
|
aria-label="Remove"
|
||||||
|
(click)="removeIpEntry(ip)"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button mat-button color="primary" (click)="createIpEntry()" type="button">
|
||||||
|
+ Add IP
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h1>SSL Certificate</h1>
|
||||||
|
<p>X.509 PEM certificate for EPP production access.</p>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="dataSource.clientCertificate"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<h1>Failover SSL Certificate</h1>
|
||||||
|
<p>X.509 PEM backup certificate for EPP Production Access.</p>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="dataSource.failoverClientCertificate"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
aria-label="Save security settings"
|
||||||
|
type="submit"
|
||||||
|
class="settings-security__edit-save"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,12 +12,22 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
.settings-security__edit {
|
||||||
|
max-width: 616px;
|
||||||
@Component({
|
h1 {
|
||||||
selector: '[app-promotions-widget]',
|
margin-top: 30px;
|
||||||
templateUrl: './promotionsWidget.component.html',
|
}
|
||||||
})
|
mat-form-field {
|
||||||
export class PromotionsWidgetComponent {
|
width: 100%;
|
||||||
constructor() {}
|
flex: 1;
|
||||||
|
}
|
||||||
|
&-ip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
&-save {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
// 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 } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||||
|
import {
|
||||||
|
SecurityService,
|
||||||
|
SecuritySettings,
|
||||||
|
apiToUiConverter,
|
||||||
|
ipAllowListItem,
|
||||||
|
} from './security.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-security-edit',
|
||||||
|
templateUrl: './securityEdit.component.html',
|
||||||
|
styleUrls: ['./securityEdit.component.scss'],
|
||||||
|
})
|
||||||
|
export default class SecurityEditComponent {
|
||||||
|
dataSource: SecuritySettings = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public securityService: SecurityService,
|
||||||
|
private _snackBar: MatSnackBar,
|
||||||
|
public registrarService: RegistrarService
|
||||||
|
) {
|
||||||
|
this.dataSource = apiToUiConverter(registrarService.registrar());
|
||||||
|
}
|
||||||
|
|
||||||
|
createIpEntry() {
|
||||||
|
this.dataSource.ipAddressAllowList?.push({ value: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.securityService.saveChanges(this.dataSource).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
this.goBack();
|
||||||
|
},
|
||||||
|
error: (err: HttpErrorResponse) => {
|
||||||
|
this._snackBar.open(err.error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.securityService.isEditingSecurity = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeIpEntry(ip: ipAllowListItem) {
|
||||||
|
this.dataSource.ipAddressAllowList =
|
||||||
|
this.dataSource.ipAddressAllowList?.filter((item) => item !== ip);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,37 @@
|
||||||
<div class="console-settings">
|
<app-selected-registrar-wrapper>
|
||||||
<h1>Settings</h1>
|
<div class="console-settings">
|
||||||
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
|
<h1 class="mat-headline-4">Settings</h1>
|
||||||
<a mat-tab-link routerLink="contact" routerLinkActive="active-link"
|
<nav
|
||||||
>Contact Info</a
|
mat-tab-nav-bar
|
||||||
|
mat-stretch-tabs="false"
|
||||||
|
mat-align-tabs="start"
|
||||||
|
[tabPanel]="tabPanel"
|
||||||
>
|
>
|
||||||
<a mat-tab-link routerLink="whois" routerLinkActive="active-link"
|
<a
|
||||||
>WHOIS Info</a
|
mat-tab-link
|
||||||
>
|
routerLink="contact"
|
||||||
<a mat-tab-link routerLink="security" routerLinkActive="active-link"
|
routerLinkActive="active-link"
|
||||||
>Security</a
|
queryParamsHandling="merge"
|
||||||
>
|
>Contacts</a
|
||||||
<a mat-tab-link routerLink="epp-password" routerLinkActive="active-link"
|
>
|
||||||
>EPP Password</a
|
<a
|
||||||
>
|
mat-tab-link
|
||||||
<a mat-tab-link routerLink="users" routerLinkActive="active-link">Users</a>
|
routerLink="whois"
|
||||||
<a mat-tab-link routerLink="registrars" routerLinkActive="active-link"
|
routerLinkActive="active-link"
|
||||||
>Registrars</a
|
queryParamsHandling="merge"
|
||||||
>
|
>WHOIS Info</a
|
||||||
</nav>
|
>
|
||||||
<mat-tab-nav-panel #tabPanel>
|
<a
|
||||||
<router-outlet></router-outlet>
|
mat-tab-link
|
||||||
</mat-tab-nav-panel>
|
routerLink="security"
|
||||||
</div>
|
routerLinkActive="active-link"
|
||||||
|
queryParamsHandling="merge"
|
||||||
|
>Security</a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-tab-nav-panel #tabPanel>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</mat-tab-nav-panel>
|
||||||
|
</div>
|
||||||
|
</app-selected-registrar-wrapper>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,6 +13,9 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
.console-settings {
|
.console-settings {
|
||||||
|
> mat-divider {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
.mdc-tab {
|
.mdc-tab {
|
||||||
&.active-link {
|
&.active-link {
|
||||||
border-bottom: 2px solid var(--primary);
|
border-bottom: 2px solid var(--primary);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -22,4 +22,10 @@ import { Component, ViewEncapsulation } from '@angular/core';
|
||||||
})
|
})
|
||||||
export class SettingsComponent {
|
export class SettingsComponent {
|
||||||
public static PATH = 'settings';
|
public static PATH = 'settings';
|
||||||
|
|
||||||
|
public static matchesUrl(url: string): boolean {
|
||||||
|
return url[0] === '/'
|
||||||
|
? url.startsWith(`/${this.PATH}`)
|
||||||
|
: url.startsWith(this.PATH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,244 +1,105 @@
|
||||||
<div class="settings-whois">
|
@if(whoisService.editing) {
|
||||||
<h2>WHOIS settings</h2>
|
<app-whois-edit></app-whois-edit>
|
||||||
<h3>
|
} @else {
|
||||||
General registrar information for your WHOIS record. This information is
|
<div class="console-app__whois">
|
||||||
always visible in WHOIS.
|
<div class="console-app__whois-controls">
|
||||||
</h3>
|
<span>
|
||||||
<div *ngIf="loading" class="settings-whois__loading">
|
General registrar information for your WHOIS record. This information is
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
always visible in WHOIS.
|
||||||
</div>
|
</span>
|
||||||
<div class="settings-whois__section">
|
<div class="spacer"></div>
|
||||||
<div class="settings-whois__section-description">
|
<button
|
||||||
<h3>Name:</h3>
|
mat-flat-button
|
||||||
</div>
|
color="primary"
|
||||||
<div class="settings-whois__section-form">
|
aria-label="Edit WHOIS record"
|
||||||
<mat-form-field>
|
(click)="whoisService.editing = true"
|
||||||
<input
|
>
|
||||||
matInput
|
<mat-icon>edit</mat-icon>
|
||||||
type="text"
|
Edit
|
||||||
[(ngModel)]="registrar.registrarName"
|
</button>
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>IANA Identifier:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.ianaIdentifier"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>ICANN Referral Email:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="email"
|
|
||||||
[(ngModel)]="registrar.icannReferralEmail"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>WHOIS server:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.whoisServer"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Referral URL:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.url"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Email:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="email"
|
|
||||||
[(ngModel)]="registrar.emailAddress"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Phone::</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.phoneNumber"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Fax:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.faxNumber"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-address">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Address Line 1:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field *ngIf="registrar.localizedAddress?.street">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="(registrar.localizedAddress?.street)![0]"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-address">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>City:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field *ngIf="registrar.localizedAddress">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.localizedAddress.city"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-address">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Address Line 2:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field *ngIf="registrar.localizedAddress?.street">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="(registrar.localizedAddress?.street)![1]"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-address">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>State/Region:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field *ngIf="registrar.localizedAddress">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.localizedAddress.state"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section">
|
|
||||||
<div class="settings-whois__section-address">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Address Line 3:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field *ngIf="registrar.localizedAddress?.street">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="(registrar.localizedAddress?.street)![2]"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-address">
|
|
||||||
<div class="settings-whois__section-description">
|
|
||||||
<h3>Country Code:</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__section-form">
|
|
||||||
<mat-form-field *ngIf="registrar.localizedAddress">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="registrar.localizedAddress.countryCode"
|
|
||||||
[disabled]="!inEdit"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-whois__actions">
|
|
||||||
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
|
||||||
<button
|
|
||||||
class="actions-save"
|
|
||||||
mat-raised-button
|
|
||||||
color="primary"
|
|
||||||
(click)="save()"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button class="actions-cancel" mat-stroked-button (click)="cancel()">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #inView>
|
|
||||||
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-list role="list">
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<h2>Personal Info</h2>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Company name</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.registrarName
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Referral email</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.emailAddress
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Phone</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.phoneNumber
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Fax</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.faxNumber
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Address</span>
|
||||||
|
<span class="console-app__list-value">{{ formattedAddress() }}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-list role="list">
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<h2>Technical Info</h2>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">IANA Identifier</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.ianaIdentifier
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<div>
|
||||||
|
<span class="console-app__list-key">ICANN Referral Email</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.icannReferralEmail
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">WHOIS server</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.whoisServer
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-list-item role="listitem">
|
||||||
|
<span class="console-app__list-key">Referral URL</span>
|
||||||
|
<span class="console-app__list-value">{{
|
||||||
|
registrarService.registrar()?.url
|
||||||
|
}}</span>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
|
@ -1,59 +1,17 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
.console-app__whois {
|
||||||
//
|
max-width: 616px;
|
||||||
// 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.
|
|
||||||
|
|
||||||
.settings-whois {
|
&-controls {
|
||||||
margin-top: 1.5rem;
|
|
||||||
&__section {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
gap: 1rem;
|
||||||
min-width: 400px;
|
margin: 20px 0;
|
||||||
}
|
|
||||||
&__section-address {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
min-width: 400px;
|
|
||||||
width: 50%;
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
&__section-description {
|
|
||||||
display: inline-block;
|
|
||||||
margin-block-start: 1em;
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
&__section-form {
|
|
||||||
display: inline-block;
|
|
||||||
width: 70%;
|
|
||||||
mat-form-field {
|
|
||||||
width: 90%;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
input:disabled {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__loading {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
&__actions {
|
|
||||||
margin-top: 50px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-right: 50px;
|
|
||||||
button {
|
button {
|
||||||
margin-left: 20px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mat-card {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,13 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { Component, computed } from '@angular/core';
|
||||||
import { Component } from '@angular/core';
|
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import {
|
|
||||||
Registrar,
|
|
||||||
RegistrarService,
|
|
||||||
} from 'src/app/registrar/registrar.service';
|
|
||||||
|
|
||||||
import { WhoisService } from './whois.service';
|
import { WhoisService } from './whois.service';
|
||||||
|
|
||||||
|
@ -26,51 +21,29 @@ import { WhoisService } from './whois.service';
|
||||||
selector: 'app-whois',
|
selector: 'app-whois',
|
||||||
templateUrl: './whois.component.html',
|
templateUrl: './whois.component.html',
|
||||||
styleUrls: ['./whois.component.scss'],
|
styleUrls: ['./whois.component.scss'],
|
||||||
providers: [WhoisService],
|
|
||||||
})
|
})
|
||||||
export default class WhoisComponent {
|
export default class WhoisComponent {
|
||||||
public static PATH = 'whois';
|
public static PATH = 'whois';
|
||||||
loading = false;
|
formattedAddress = computed(() => {
|
||||||
inEdit = false;
|
let result = '';
|
||||||
registrar: Registrar;
|
const registrar = this.registrarService.registrar();
|
||||||
|
if (registrar?.localizedAddress?.street) {
|
||||||
|
result += `${registrar?.localizedAddress?.street?.join(' ')} `;
|
||||||
|
}
|
||||||
|
if (registrar?.localizedAddress?.city) {
|
||||||
|
result += `${registrar?.localizedAddress?.city} `;
|
||||||
|
}
|
||||||
|
if (registrar?.localizedAddress?.state) {
|
||||||
|
result += `${registrar?.localizedAddress?.state} `;
|
||||||
|
}
|
||||||
|
if (registrar?.localizedAddress?.street) {
|
||||||
|
result += registrar?.localizedAddress?.countryCode;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public whoisService: WhoisService,
|
public whoisService: WhoisService,
|
||||||
public registrarService: RegistrarService,
|
public registrarService: RegistrarService
|
||||||
private _snackBar: MatSnackBar
|
) {}
|
||||||
) {
|
|
||||||
this.registrar = JSON.parse(
|
|
||||||
JSON.stringify(this.registrarService.registrar)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
enableEdit() {
|
|
||||||
this.inEdit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.inEdit = false;
|
|
||||||
this.resetDataSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.loading = true;
|
|
||||||
this.whoisService.saveChanges(this.registrar).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
this.loading = false;
|
|
||||||
this.resetDataSource();
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this._snackBar.open(err.error);
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDataSource() {
|
|
||||||
this.registrar = JSON.parse(
|
|
||||||
JSON.stringify(this.registrarService.registrar)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -14,21 +14,17 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { switchMap } from 'rxjs';
|
import { switchMap } from 'rxjs';
|
||||||
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
|
import {
|
||||||
|
RegistrarService,
|
||||||
|
WhoisRegistrarFields,
|
||||||
|
} 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 WhoisRegistrarFields {
|
@Injectable({
|
||||||
ianaIdentifier?: number;
|
providedIn: 'root',
|
||||||
icannReferralEmail?: string;
|
})
|
||||||
localizedAddress?: Address;
|
|
||||||
registrarId?: string;
|
|
||||||
url?: string;
|
|
||||||
whoisServer?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WhoisService {
|
export class WhoisService {
|
||||||
whoisRegistrarFields: WhoisRegistrarFields = {};
|
editing: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private backend: BackendService,
|
private backend: BackendService,
|
||||||
|
|
128
console-webapp/src/app/settings/whois/whoisEdit.component.html
Normal file
128
console-webapp/src/app/settings/whois/whoisEdit.component.html
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<div class="console-app__whois-edit" *ngIf="registrarInEdit">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
class="console-app__whois-edit-back"
|
||||||
|
aria-label="Back to whois view"
|
||||||
|
(click)="whoisService.editing = false"
|
||||||
|
>
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="console-app__whois-edit-controls">
|
||||||
|
<span>
|
||||||
|
General registrar information for your WHOIS record. This information is
|
||||||
|
always visible in WHOIS.
|
||||||
|
</span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="console-app__whois-edit">
|
||||||
|
<h1>Personal info</h1>
|
||||||
|
|
||||||
|
<form (ngSubmit)="save($event)">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Email: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="email"
|
||||||
|
[(ngModel)]="registrarInEdit.emailAddress"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Phone: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="registrarInEdit.phoneNumber"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Fax: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="registrarInEdit.faxNumber"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Street Address: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
[(ngModel)]="registrarInEdit.localizedAddress.street![1]"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>City: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
[(ngModel)]="(registrarInEdit.localizedAddress || {}).city"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>State or Province: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
[(ngModel)]="(registrarInEdit.localizedAddress || {}).state"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Country: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
[(ngModel)]="(registrarInEdit.localizedAddress || {}).countryCode"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Postal code: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
[(ngModel)]="(registrarInEdit.localizedAddress || {}).zip"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<h1>Technical info</h1>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>WHOIS server: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="registrarInEdit.whoisServer"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Referral URL: </mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="registrarInEdit.url"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-flat-button color="primary" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,19 @@
|
||||||
|
.console-app__whois-edit {
|
||||||
|
max-width: 616px;
|
||||||
|
|
||||||
|
&-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 20px 0;
|
||||||
|
button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
58
console-webapp/src/app/settings/whois/whoisEdit.component.ts
Normal file
58
console-webapp/src/app/settings/whois/whoisEdit.component.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// 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, effect } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import {
|
||||||
|
Registrar,
|
||||||
|
RegistrarService,
|
||||||
|
} from 'src/app/registrar/registrar.service';
|
||||||
|
import { WhoisService } from './whois.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-whois-edit',
|
||||||
|
templateUrl: './whoisEdit.component.html',
|
||||||
|
styleUrls: ['./whoisEdit.component.scss'],
|
||||||
|
})
|
||||||
|
export default class WhoisEditComponent {
|
||||||
|
registrarInEdit: Registrar | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public whoisService: WhoisService,
|
||||||
|
public registrarService: RegistrarService,
|
||||||
|
private _snackBar: MatSnackBar
|
||||||
|
) {
|
||||||
|
effect(() => {
|
||||||
|
const registrar = this.registrarService.registrar();
|
||||||
|
if (registrar) {
|
||||||
|
this.registrarInEdit = structuredClone(registrar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
save(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.registrarInEdit) return;
|
||||||
|
|
||||||
|
this.whoisService.saveChanges(this.registrarInEdit).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
this.whoisService.editing = false;
|
||||||
|
},
|
||||||
|
error: (err: HttpErrorResponse) => {
|
||||||
|
this._snackBar.open(err.error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
// 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 { BreakpointObserver } from '@angular/cdk/layout';
|
|
||||||
import { ComponentType } from '@angular/cdk/portal';
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import {
|
|
||||||
MatBottomSheet,
|
|
||||||
MatBottomSheetRef,
|
|
||||||
} from '@angular/material/bottom-sheet';
|
|
||||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
|
||||||
|
|
||||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
|
||||||
|
|
||||||
export interface DialogBottomSheetContent {
|
|
||||||
init(data: Object): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps up a child component in an Angular Material Dalog for desktop or a Bottom Sheet
|
|
||||||
* component for mobile depending on a screen resolution, with Breaking Point being 599px.
|
|
||||||
* Child component is required to implement @see DialogBottomSheetContent interface
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'app-dialog-bottom-sheet-wrapper',
|
|
||||||
template: '',
|
|
||||||
})
|
|
||||||
export class DialogBottomSheetWrapper {
|
|
||||||
private elementRef?: MatBottomSheetRef | MatDialogRef<any>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private dialog: MatDialog,
|
|
||||||
private bottomSheet: MatBottomSheet,
|
|
||||||
protected breakpointObserver: BreakpointObserver
|
|
||||||
) {}
|
|
||||||
|
|
||||||
open<T extends DialogBottomSheetContent>(
|
|
||||||
component: ComponentType<T>,
|
|
||||||
data: any
|
|
||||||
) {
|
|
||||||
const config = { data, close: () => this.close() };
|
|
||||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
|
||||||
this.elementRef = this.bottomSheet.open(component);
|
|
||||||
this.elementRef.instance.init(config);
|
|
||||||
} else {
|
|
||||||
this.elementRef = this.dialog.open(component);
|
|
||||||
this.elementRef.componentInstance.init(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
|
||||||
this.elementRef.dismiss();
|
|
||||||
} else if (this.elementRef instanceof MatDialogRef) {
|
|
||||||
this.elementRef.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="console-app__notifications">
|
||||||
|
@for (notification of mockNotifications; track notification.id) {
|
||||||
|
<div class="console-app__notification">
|
||||||
|
<div class="console-app__notification-content">
|
||||||
|
<button mat-mini-fab color="primary" disabled>
|
||||||
|
<mat-icon mat-mini-fab color="primary"> calendar_month </mat-icon>
|
||||||
|
</button>
|
||||||
|
<h4>{{ notification.text }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button mat-icon-button>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
.console-app {
|
||||||
|
&__notifications {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
&__notification {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--lightest);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -13,17 +13,25 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { DomainListComponent } from 'src/app/domains/domainList.component';
|
interface Notification {
|
||||||
|
id: string;
|
||||||
|
date: Date;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[app-domains-widget]',
|
selector: 'app-notifications',
|
||||||
templateUrl: './domainsWidget.component.html',
|
templateUrl: './notifications.component.html',
|
||||||
|
styleUrls: ['./notifications.component.scss'],
|
||||||
})
|
})
|
||||||
export class DomainsWidgetComponent {
|
export class NotificationsComponent {
|
||||||
constructor(private router: Router) {}
|
protected mockNotifications: Notification[] = [
|
||||||
|
{
|
||||||
openDomainsPage() {
|
id: '0',
|
||||||
this.router.navigate([DomainListComponent.PATH]);
|
date: new Date(),
|
||||||
}
|
text: 'Registry Downtime Planned on June 9, 2024 due to maintenance.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
constructor() {}
|
||||||
}
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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 { Component } from '@angular/core';
|
||||||
|
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-selected-registrar-wrapper',
|
||||||
|
styleUrls: ['./selectedRegistrarWrapper.component.scss'],
|
||||||
|
template: `
|
||||||
|
@if(registrarService.registrarId()){
|
||||||
|
<ng-content></ng-content>
|
||||||
|
} @else {
|
||||||
|
<div class="console-app__emty-registrar">
|
||||||
|
<h1>
|
||||||
|
<mat-icon class="console-app__emty-registrar-icon">block</mat-icon>
|
||||||
|
</h1>
|
||||||
|
<h1>No registrar selected</h1>
|
||||||
|
<h4 class="mat-body-2">Please select a registrar</h4>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class SelectedRegistrarWrapper {
|
||||||
|
constructor(protected registrarService: RegistrarService) {}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,12 +12,17 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Location } from '@angular/common';
|
||||||
|
import { Directive, HostListener } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Directive({
|
||||||
selector: '[app-tlds-widget]',
|
selector: '[backButton]',
|
||||||
templateUrl: './tldsWidget.component.html',
|
|
||||||
})
|
})
|
||||||
export class TldsWidgetComponent {
|
export class LocationBackDirective {
|
||||||
constructor() {}
|
constructor(private location: Location) {}
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
onClick() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -14,14 +14,16 @@
|
||||||
|
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { catchError, Observable, of } from 'rxjs';
|
import { Observable, catchError, of, throwError } from 'rxjs';
|
||||||
import { SecuritySettingsBackendModel } from 'src/app/settings/security/security.service';
|
import { SecuritySettingsBackendModel } from 'src/app/settings/security/security.service';
|
||||||
|
|
||||||
import { Contact } from '../../settings/contact/contact.service';
|
|
||||||
import { Registrar } from '../../registrar/registrar.service';
|
|
||||||
import { UserData } from './userData.service';
|
|
||||||
import { WhoisRegistrarFields } from 'src/app/settings/whois/whois.service';
|
|
||||||
import { DomainListResult } from 'src/app/domains/domainList.service';
|
import { DomainListResult } from 'src/app/domains/domainList.service';
|
||||||
|
import {
|
||||||
|
Registrar,
|
||||||
|
WhoisRegistrarFields,
|
||||||
|
} from '../../registrar/registrar.service';
|
||||||
|
import { Contact } from '../../settings/contact/contact.service';
|
||||||
|
import { UserData } from './userData.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackendService {
|
export class BackendService {
|
||||||
|
@ -42,8 +44,11 @@ export class BackendService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return throwError(() => {throw "Failed"});
|
if (mockData) {
|
||||||
return of(<Type>mockData);
|
return of(<Type>mockData);
|
||||||
|
} else {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getContacts(registrarId: string): Observable<Contact[]> {
|
getContacts(registrarId: string): Observable<Contact[]> {
|
||||||
|
@ -99,6 +104,12 @@ export class BackendService {
|
||||||
.pipe(catchError((err) => this.errorCatcher<Registrar[]>(err)));
|
.pipe(catchError((err) => this.errorCatcher<Registrar[]>(err)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postRegistrar(registrar: Registrar): Observable<Registrar> {
|
||||||
|
return this.http
|
||||||
|
.post<Registrar>('/console-api/registrar', registrar)
|
||||||
|
.pipe(catchError((err) => this.errorCatcher<Registrar>(err)));
|
||||||
|
}
|
||||||
|
|
||||||
getSecuritySettings(
|
getSecuritySettings(
|
||||||
registrarId: string
|
registrarId: string
|
||||||
): Observable<SecuritySettingsBackendModel> {
|
): Observable<SecuritySettingsBackendModel> {
|
||||||
|
|
45
console-webapp/src/app/shared/services/breakPoint.service.ts
Normal file
45
console-webapp/src/app/shared/services/breakPoint.service.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// 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 { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
import { distinctUntilChanged } from 'rxjs';
|
||||||
|
|
||||||
|
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 560px)';
|
||||||
|
const TABLET_LAYOUT_BREAKPOINT = '(max-width: 768px)';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class BreakPointObserverService {
|
||||||
|
isMobileView = signal<boolean>(false);
|
||||||
|
isTabletView = signal<boolean>(false);
|
||||||
|
|
||||||
|
readonly breakpoint$ = this.breakpointObserver
|
||||||
|
.observe([MOBILE_LAYOUT_BREAKPOINT, TABLET_LAYOUT_BREAKPOINT])
|
||||||
|
.pipe(distinctUntilChanged());
|
||||||
|
|
||||||
|
constructor(protected breakpointObserver: BreakpointObserver) {
|
||||||
|
this.breakpoint$.subscribe(() => this.breakpointChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
private breakpointChanged() {
|
||||||
|
this.isMobileView.set(
|
||||||
|
this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)
|
||||||
|
);
|
||||||
|
this.isTabletView.set(
|
||||||
|
this.breakpointObserver.isMatched(TABLET_LAYOUT_BREAKPOINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue