Replace less with scss, add homepage widgets, add registrars selector and registrars page (#2114)

* Replace less compiler with scss

* Replace less with scss, add homepage widgets, add registrars selector and registrars page
This commit is contained in:
Pavlo Tkach 2023-09-01 11:48:30 -04:00 committed by GitHub
parent 6b5ec36eed
commit 1929654f8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 11927 additions and 730 deletions

View file

@ -7,7 +7,7 @@
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "less"
"style": "scss"
}
},
"root": "",
@ -22,15 +22,18 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.less"
"src/theme.scss",
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": ["node_modules/"]
},
"scripts": []
},
"configurations": {
@ -91,15 +94,18 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "less",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.less"
"src/theme.scss",
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": ["node_modules/"]
},
"scripts": []
}
},

File diff suppressed because it is too large Load diff

View file

@ -18,51 +18,56 @@ import { TldsComponent } from './tlds/tlds.component';
import { HomeComponent } from './home/home.component';
import { SettingsComponent } from './settings/settings.component';
import SettingsContactComponent from './settings/contact/contact.component';
import SettingsRegistrarsComponent from './settings/registrars/registrars.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/registrar.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'registrars', component: RegistrarComponent },
{ path: 'empty-registrar', component: EmptyRegistrar },
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{
path: 'settings',
component: SettingsComponent,
canActivate: [RegistrarGuard],
children: [
{
path: '',
redirectTo: 'contact',
redirectTo: 'registrars',
pathMatch: 'full',
},
{
path: 'contact',
component: SettingsContactComponent,
canActivate: [RegistrarGuard],
},
{
path: 'whois',
component: SettingsWhoisComponent,
canActivate: [RegistrarGuard],
},
{
path: 'security',
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'epp-password',
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'users',
component: SettingsUsersComponent,
canActivate: [RegistrarGuard],
},
{
path: 'registrars',
component: SettingsRegistrarsComponent,
component: RegistrarComponent,
},
],
},

View file

@ -12,13 +12,13 @@
<a mat-list-item [routerLink]="'/settings'" routerLinkActive="active">
Settings
</a>
<a mat-list-item [routerLink]="'/registrars'" routerLinkActive="active">
Select Registrar
</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content">
<div *ngIf="globalLoader.isLoading" class="console-app__global-spinner">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="console-app__content" *ngIf="renderRouter">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>

View file

@ -13,8 +13,8 @@
// limitations under the License.
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol" !important;
font-size: 14px;
color: #333;
box-sizing: border-box;
@ -47,4 +47,7 @@
max-width: 1340px;
margin: 0 auto;
}
&__global-spinner {
margin-bottom: 2rem;
}
}

View file

@ -13,10 +13,25 @@
// limitations under the License.
import { Component } from '@angular/core';
import { RegistrarService } from './registrar/registrar.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less'],
styleUrls: ['./app.component.scss'],
})
export class AppComponent {}
export class AppComponent {
renderRouter: boolean = true;
constructor(
protected registrarService: RegistrarService,
protected globalLoader: GlobalLoaderService
) {
registrarService.activeRegistrarIdChange.subscribe(() => {
this.renderRouter = false;
setTimeout(() => {
this.renderRouter = true;
}, 400);
});
}
}

View file

@ -31,9 +31,20 @@ import SettingsContactComponent, {
ContactDetailsDialogComponent,
} from './settings/contact/contact.component';
import { HttpClientModule } from '@angular/common/http';
import { RegistrarComponent } from './registrar/registrar.component';
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 { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import { RegistrarSelectorComponent } from './registrar/registrar-selector.component';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { ContactWidgetComponent } from './home/widgets/contact-widget.component';
import { PromotionsWidgetComponent } from './home/widgets/promotions-widget.component';
import { TldsWidgetComponent } from './home/widgets/tlds-widget.component';
import { ResourcesWidgetComponent } from './home/widgets/resources-widget.component';
import { EppWidgetComponent } from './home/widgets/epp-widget.component';
import { BillingWidgetComponent } from './home/widgets/billing-widget.component';
import { DomainsWidgetComponent } from './home/widgets/domains-widget.component';
@NgModule({
declarations: [
@ -46,6 +57,15 @@ import SecurityComponent from './settings/security/security.component';
ContactDetailsDialogComponent,
RegistrarComponent,
SecurityComponent,
EmptyRegistrar,
RegistrarSelectorComponent,
ContactWidgetComponent,
DomainsWidgetComponent,
PromotionsWidgetComponent,
TldsWidgetComponent,
ResourcesWidgetComponent,
EppWidgetComponent,
BillingWidgetComponent,
],
imports: [
HttpClientModule,
@ -55,7 +75,17 @@ import SecurityComponent from './settings/security/security.component';
AppRoutingModule,
BrowserAnimationsModule,
],
providers: [BackendService, RegistrarGuard],
providers: [
GlobalLoaderService,
BackendService,
RegistrarGuard,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
subscriptSizing: 'dynamic',
},
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

View file

@ -5,6 +5,7 @@
</button>
<span>Google Registry</span>
<span class="spacer"></span>
<app-registrar-selector />
<button mat-icon-button aria-label="Open FAQ">
<mat-icon>question_mark</mat-icon>
</button>

View file

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
.console-app__header {
margin-top: 0;
}
.spacer {
flex: 1;
}

View file

@ -17,7 +17,7 @@ import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.less'],
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
private isNavOpen = false;

View file

@ -1,21 +1,13 @@
<h3>Recent Activity</h3>
<table
mat-table
[dataSource]="dataSource"
class="mat-elevation-z8 console-home__activity"
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef>
{{ column.header }}
</th>
<td mat-cell *matCellDef="let row">
{{ column.cell(row) }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<div class="console-app__home">
<h1>Welcome to the Google Registry Console</h1>
<div class="console-app__home-widgets">
<div app-domains-widget class="console-app__widget-wrapper__wide"></div>
<div app-contact-widget class="console-app__widget-wrapper__wide"></div>
<div app-tlds-widget></div>
<div app-promotions-widget class="console-app__widget-wrapper__wide"></div>
<div app-promotions-widget class="console-app__widget-wrapper__wide"></div>
<div app-resources-widget></div>
<div app-billing-widget></div>
<div app-epp-widget></div>
</div>
</div>

View file

@ -1,13 +0,0 @@
// Copyright 2022 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.

View file

@ -12,11 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
html,
body {
.console-app {
&__home {
margin-top: 1rem;
}
&__home-widgets {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-gap: 20px;
mat-card {
height: 100%;
h1 {
margin-top: 1rem;
}
}
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}

View file

@ -12,220 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
export interface ActivityRecord {
eventType: string;
userName: string;
registrarName: string;
timestamp: string;
details: string;
}
const MOCK_DATA: ActivityRecord[] = [
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
];
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.less'],
styleUrls: ['./home.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class HomeComponent {
columns = [
{
columnDef: 'eventType',
header: 'Event Type',
cell: (record: ActivityRecord) => `${record.eventType}`,
},
{
columnDef: 'userName',
header: 'User',
cell: (record: ActivityRecord) => `${record.userName}`,
},
{
columnDef: 'registrarName',
header: 'Registrar',
cell: (record: ActivityRecord) => `${record.registrarName}`,
},
{
columnDef: 'timestamp',
header: 'Timestamp',
cell: (record: ActivityRecord) => `${record.timestamp}`,
},
{
columnDef: 'details',
header: 'Details',
cell: (record: ActivityRecord) => `${record.details}`,
},
];
dataSource = MOCK_DATA;
displayedColumns = this.columns.map((c) => c.columnDef);
}
export class HomeComponent {}

View file

@ -0,0 +1,13 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<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">
View important billing and payments information.
</h4>
</div>
</div>
</mat-card-content>
</mat-card>

View file

@ -15,8 +15,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-registrars',
templateUrl: './registrars.component.html',
styleUrls: ['./registrars.component.less'],
selector: '[app-billing-widget]',
templateUrl: './billing-widget.component.html',
})
export default class RegistrarsComponent {}
export class BillingWidgetComponent {
constructor() {}
}

View file

@ -0,0 +1,27 @@
<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">
View Google Registry support email and phone information
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
Give us a Call
</button>
<p class="secondary-text">
Call Google Registry support at +1 (404) 978 8419
</p>
<button mat-button color="primary" class="console-app__widget-link">
Send us an Email
</button>
<p class="secondary-text">
Email Google Registry at support@google.com
</p>
</div>
</div>
</mat-card-content>
</mat-card>

View file

@ -0,0 +1,23 @@
// 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';
@Component({
selector: '[app-contact-widget]',
templateUrl: './contact-widget.component.html',
})
export class ContactWidgetComponent {
constructor() {}
}

View file

@ -0,0 +1,25 @@
<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">
View DUMs
</button>
<p class="secondary-text">
Download a csv of all domains under management
</p>
</div>
</div>
</mat-card-content>
</mat-card>

View file

@ -0,0 +1,23 @@
// 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';
@Component({
selector: '[app-domains-widget]',
templateUrl: './domains-widget.component.html',
})
export class DomainsWidgetComponent {
constructor() {}
}

View file

@ -0,0 +1,13 @@
<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>

View file

@ -11,3 +11,13 @@
// 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';
@Component({
selector: '[app-epp-widget]',
templateUrl: './epp-widget.component.html',
})
export class EppWidgetComponent {
constructor() {}
}

View file

@ -0,0 +1,27 @@
<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>

View file

@ -0,0 +1,23 @@
// 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';
@Component({
selector: '[app-promotions-widget]',
templateUrl: './promotions-widget.component.html',
})
export class PromotionsWidgetComponent {
constructor() {}
}

View file

@ -0,0 +1,13 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<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>
</div>
</div>
</mat-card-content>
</mat-card>

View file

@ -0,0 +1,23 @@
// 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';
@Component({
selector: '[app-resources-widget]',
templateUrl: './resources-widget.component.html',
})
export class ResourcesWidgetComponent {
constructor() {}
}

View file

@ -0,0 +1,13 @@
<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>

View file

@ -0,0 +1,23 @@
// 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';
@Component({
selector: '[app-tlds-widget]',
templateUrl: './tlds-widget.component.html',
})
export class TldsWidgetComponent {
constructor() {}
}

View file

@ -44,6 +44,7 @@ import { CdkMenuModule } from '@angular/cdk/menu';
import { DialogModule } from '@angular/cdk/dialog';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatPaginatorModule } from '@angular/material/paginator';
@NgModule({
exports: [
@ -79,6 +80,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
OverlayModule,
DialogModule,
MatSnackBarModule,
MatPaginatorModule,
],
})
export class MaterialModule {}

View file

@ -0,0 +1,7 @@
<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>

View file

@ -0,0 +1,17 @@
.console-app {
&__emty-registrar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
&-icon {
transform: scale(3);
margin-bottom: 0.5rem;
}
}
}

View file

@ -13,33 +13,32 @@
// limitations under the License.
import { Component } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { ActivatedRoute, Router } from '@angular/router';
import { RegistrarService } from './registrar.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-registrar',
templateUrl: './registrar.component.html',
styleUrls: ['./registrar.component.less'],
selector: 'app-empty-registrar',
templateUrl: './emptyRegistrar.component.html',
styleUrls: ['./emptyRegistrar.component.scss'],
})
export class RegistrarComponent {
private lastActiveRegistrarId: string;
export class EmptyRegistrar {
private registrarIdChangeSubscription?: Subscription;
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private router: Router
) {
this.lastActiveRegistrarId = registrarService.activeRegistrarId;
}
ngDoCheck() {
if (
this.registrarService.activeRegistrarId &&
this.registrarService.activeRegistrarId !== this.lastActiveRegistrarId &&
this.route.snapshot.paramMap.get('nextUrl')
) {
this.lastActiveRegistrarId = this.registrarService.activeRegistrarId;
this.registrarIdChangeSubscription =
registrarService.activeRegistrarIdChange.subscribe((newRegistrarId) => {
if (newRegistrarId) {
this.router.navigate([this.route.snapshot.paramMap.get('nextUrl')]);
}
});
}
ngOnDestroy() {
this.registrarIdChangeSubscription?.unsubscribe();
}
}

View file

@ -0,0 +1,18 @@
<div class="console-app__registrar">
<div>
<mat-form-field class="mat-form-field-density-5" appearance="fill">
<mat-label>Registrar</mat-label>
<mat-select
[ngModel]="registrarService.activeRegistrarId"
(selectionChange)="registrarService.updateRegistrar($event.value)"
>
<mat-option
*ngFor="let registrar of registrarService.registrars"
[value]="registrar.registrarName"
>
{{ registrar.registrarName }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>

View file

@ -0,0 +1,18 @@
.console-app {
&__registrar {
display: flex;
justify-content: center;
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;
}
}

View file

@ -14,18 +14,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import RegistrarsComponent from './registrars.component';
import { RegistrarSelectorComponent } from './registrar-selector.component';
describe('RegistrarsComponent', () => {
let component: RegistrarsComponent;
let fixture: ComponentFixture<RegistrarsComponent>;
describe('RegistrarSelectorComponent', () => {
let component: RegistrarSelectorComponent;
let fixture: ComponentFixture<RegistrarSelectorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarsComponent],
declarations: [RegistrarSelectorComponent],
}).compileComponents();
fixture = TestBed.createComponent(RegistrarsComponent);
fixture = TestBed.createComponent(RegistrarSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View file

@ -0,0 +1,25 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { RegistrarService } from './registrar.service';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrar-selector.component.html',
styleUrls: ['./registrar-selector.component.scss'],
})
export class RegistrarSelectorComponent {
constructor(protected registrarService: RegistrarService) {}
}

View file

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

View file

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

View file

@ -29,6 +29,6 @@ export class RegistrarGuard {
if (this.registrarService.activeRegistrarId) {
return true;
}
return this.router.navigate([`/registrars`, { nextUrl: state.url }]);
return this.router.navigate([`/empty-registrar`, { nextUrl: state.url }]);
}
}

View file

@ -14,16 +14,59 @@
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { Subject } from 'rxjs';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
interface Address {
street?: string[];
city?: string;
countryCode?: string;
zip?: string;
state?: string;
}
export interface Registrar {
allowedTlds?: string[];
ipAddressAllowList?: string[];
emailAddress?: string;
billingAccountMap?: object;
driveFolderId?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
localizedAddress?: Address;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
}
@Injectable({
providedIn: 'root',
})
export class RegistrarService {
export class RegistrarService implements GlobalLoader {
activeRegistrarId: string = '';
registrars: string[] = [];
constructor(private backend: BackendService) {
registrars: Registrar[] = [];
activeRegistrarIdChange: Subject<string> = new Subject<string>();
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService
) {
this.backend.getRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
this.registrars = r;
});
this.globalLoader.startGlobalLoader(this);
}
public updateRegistrar(registrarId: string) {
this.activeRegistrarId = registrarId;
this.activeRegistrarIdChange.next(registrarId);
}
loadingTimeout() {
// TODO: Decide what to do when timeout happens
}
}

View file

@ -0,0 +1,25 @@
<div class="console-app__registrars">
<table
mat-table
[dataSource]="registrarService.registrars"
class="mat-elevation-z8"
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef>
{{ column.header }}
</th>
<td mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
class="mat-elevation-z8"
[pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
></mat-paginator>
</div>

View file

@ -0,0 +1,5 @@
.console-app {
&__registrars {
margin-top: 1.5rem;
}
}

View file

@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarComponent } from './registrar.component';
import { RegistrarComponent } from './registrarsTable.component';
import { BackendService } from '../shared/services/backend.service';
import { ActivatedRoute } from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';

View file

@ -0,0 +1,75 @@
// 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 { Registrar, RegistrarService } from './registrar.service';
@Component({
selector: 'app-registrar',
templateUrl: './registrarsTable.component.html',
styleUrls: ['./registrarsTable.component.scss'],
})
export class RegistrarComponent {
columns = [
{
columnDef: 'registrarId',
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 = this.columns.map((c) => c.columnDef);
constructor(protected registrarService: RegistrarService) {}
}

View file

@ -2,6 +2,15 @@
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div *ngIf="!loading">
<div
class="contact__empty-contacts"
*ngIf="contactService.contacts.length === 0"
>
<mat-icon class="contact__empty-contacts-icon secondary-text"
>apps_outage</mat-icon
>
<h1>No contacts found</h1>
</div>
<div *ngFor="let group of groupedData">
<div class="contact__cards-wrapper" *ngIf="group.contacts.length">
<h3>{{ group.label }}s</h3>

View file

@ -37,6 +37,18 @@
justify-content: flex-start;
margin: 2rem 0;
}
&__empty-contacts {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
&__empty-contacts-icon {
width: 4rem;
height: 4rem;
font-size: 4rem;
margin-top: 1.5rem;
}
}
.contact-details {
&__input {

View file

@ -77,7 +77,7 @@ class ContactDetailsEventsResponder {
@Component({
selector: 'app-contact-details-dialog',
templateUrl: 'contact-details.component.html',
styleUrls: ['./contact.component.less'],
styleUrls: ['./contact.component.scss'],
})
export class ContactDetailsDialogComponent {
contact: Contact;
@ -140,7 +140,7 @@ export class ContactDetailsDialogComponent {
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.less'],
styleUrls: ['./contact.component.scss'],
})
export default class ContactComponent {
loading: boolean = false;
@ -158,7 +158,7 @@ export default class ContactComponent {
}
public get groupedData() {
return this.contactService.contacts.reduce((acc, contact) => {
return this.contactService.contacts?.reduce((acc, contact) => {
contact.types.forEach((type) => {
acc
.find((group: GroupedContacts) => group.value === type)

View file

@ -1 +0,0 @@
<p>registrars component works!</p>

View file

@ -20,7 +20,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.less'],
styleUrls: ['./security.component.scss'],
providers: [SecurityService],
})
export default class SecurityComponent {

View file

@ -17,7 +17,7 @@ import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.less'],
styleUrls: ['./settings.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class SettingsComponent {}

View file

@ -17,6 +17,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.less'],
styleUrls: ['./users.component.scss'],
})
export default class UsersComponent {}

View file

@ -17,6 +17,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.less'],
styleUrls: ['./whois.component.scss'],
})
export default class WhoisComponent {}

View file

@ -18,6 +18,7 @@ import { catchError, Observable, of } from 'rxjs';
import { SecuritySettingsBackendModel } from 'src/app/settings/security/security.service';
import { Contact } from '../../settings/contact/contact.service';
import { Registrar } from '../../registrar/registrar.service';
@Injectable()
export class BackendService {
@ -60,10 +61,10 @@ export class BackendService {
);
}
getRegistrars(): Observable<string[]> {
getRegistrars(): Observable<Registrar[]> {
return this.http
.get<string[]>('/console-api/registrars')
.pipe(catchError((err) => this.errorCatcher<string[]>(err)));
.get<Registrar[]>('/console-api/registrars')
.pipe(catchError((err) => this.errorCatcher<Registrar[]>(err)));
}
getSecuritySettings(

View file

@ -0,0 +1,60 @@
// 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 { Observable, Subscription, timeout } from 'rxjs';
export interface GlobalLoader {
loadingTimeout: () => void;
}
/**
* This class responsible for global application loading indicator
*
* <p>Only to be used for when the activity should indicate that the entire application is busy
* For instance - when initial user information is loading, which is crucial for any subsequent
* interaction with the application
*/
@Injectable({
providedIn: 'root',
})
export class GlobalLoaderService {
private static readonly TIMEOUT_MS = 3000;
private loaders = new Map<GlobalLoader, Subscription>();
public isLoading: boolean = false;
private syncLoading() {
this.isLoading = this.loaders.size > 0;
}
startGlobalLoader(c: GlobalLoader) {
const subscription = new Observable(() => {})
.pipe(timeout(GlobalLoaderService.TIMEOUT_MS))
.subscribe({
error: () => {
this.loaders.delete(c);
c.loadingTimeout();
this.syncLoading();
},
});
this.loaders.set(c, subscription);
this.syncLoading();
}
stopGlobalLoader(c: GlobalLoader) {
this.loaders.get(c)?.unsubscribe();
this.loaders.delete(c);
this.syncLoading();
}
}

View file

@ -17,6 +17,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-tlds',
templateUrl: './tlds.component.html',
styleUrls: ['./tlds.component.less'],
styleUrls: ['./tlds.component.scss'],
})
export class TldsComponent {}

View file

@ -0,0 +1,63 @@
// Copyright 2022 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.
@use "@angular/material" as mat;
@import "app/registrar/registrar-selector.component.scss";
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.text-center {
text-align: center;
}
.console-app {
&__widget {
display: flex;
gap: 10px;
&-wrapper__wide {
grid-column: span 2;
}
&-link {
padding: 0 !important;
text-align: left;
margin-bottom: 0.5rem;
}
&-title {
color: var(--primary) !important;
}
&-icon {
font-size: 4rem;
line-height: 4rem;
height: 4rem !important;
width: 4rem !important;
}
&_left {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
&_right {
flex: 1;
border-left: 1px solid var(--secondary);
padding-left: 20px;
}
}
}

View file

@ -0,0 +1,69 @@
@use "sass:map";
@use "@angular/material" as mat;
/** Copied from docs section **/
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$theme-primary: mat.define-palette(mat.$indigo-palette);
$theme-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
// The warn palette is optional (defaults to red).
$theme-warn: mat.define-palette(mat.$red-palette);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
/** Application specific section **/
@mixin form-field-density($density) {
$field-typography: mat.define-typography-config(
$body-1: mat.define-typography-level(12px, 24px, 400),
);
@include mat.typography-level($field-typography, "body-1");
@include mat.form-field-density($density);
}
// Define lowest possible density class to be used in application
// In the same manner -1...-5 classes can be defined
.mat-form-field-density-5 {
@include form-field-density(-5);
}
$foreground: map.merge($theme, mat.$light-theme-foreground-palette);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get($foreground, "secondary-text");
}
:root {
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get($foreground, "secondary-text")};
}
@include mat.all-component-themes($theme);
@import "@angular/material/theming";
// Define application specific typography settings, font-family, etc
$typography-configuration: mat-typography-config(
$font-family: 'Roboto, "Helvetica Neue", sans-serif',
);
@include angular-material-typography($typography-configuration);