Restyle registrar console based on the new design proposal (#2336)

This commit is contained in:
Pavlo Tkach
2024-03-21 22:05:09 +00:00
committed by GitHub
parent de3af34b66
commit 59f4129ee0
119 changed files with 3468 additions and 2846 deletions
@@ -1,48 +1,40 @@
@if (loading) {
<div class="contact__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
@if(contactService.isContactDetailsView || contactService.isContactNewView) {
<app-contact-details></app-contact-details>
} @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) {
<div class="contact__empty-contacts">
<mat-icon class="contact__empty-contacts-icon secondary-text"
<div class="console-app__empty-contacts">
<mat-icon class="console-app__empty-contacts-icon secondary-text"
>apps_outage</mat-icon
>
<h1>No contacts found</h1>
</div>
} @else { @for (group of groupedContacts(); track group.emailAddress) {
<div class="contact__cards-wrapper">
<h3>{{ group.label }}s</h3>
<mat-divider></mat-divider>
<div class="contact__cards">
<mat-card class="contact__card" *ngFor="let contact of group.contacts">
<mat-card-title>{{ contact.name }}</mat-card-title>
<p *ngIf="contact.phoneNumber">{{ contact.phoneNumber }}</p>
<p *ngIf="contact.emailAddress">{{ contact.emailAddress }}</p>
<mat-card-actions class="contact__card-actions">
<button
mat-button
color="primary"
(click)="openDetails($event, contact)"
>
<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>
} @else {
<mat-table [dataSource]="dataSource" class="mat-elevation-z0">
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row)"
></mat-row>
</mat-table>
}
</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");
// 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
// limitations under the License.
.contact {
&__cards-wrapper {
margin-top: 22px;
}
&__card {
width: 400px;
padding: 15px;
}
&__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;
.console-app {
&__contacts {
margin-top: -10px;
width: 100%;
overflow: auto;
mat-table {
min-width: 800px;
}
}
&__empty-contacts {
display: flex;
@@ -49,22 +33,14 @@
font-size: 4rem;
margin-top: 1.5rem;
}
}
.contact-details {
&__input {
width: 100%;
&__contacts-controls {
position: absolute;
right: 20px;
top: 5px;
}
&__group {
margin: 20px 0;
display: flex;
align-items: baseline;
}
&__group-content {
display: flex;
flex-wrap: wrap;
max-width: 450px;
* {
min-width: 200px;
}
.contact__name-column-title {
color: #5f6368;
font-weight: 500;
padding: 10px 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");
// 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");
// 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
// limitations under the License.
import { Component, ViewChild, computed } from '@angular/core';
import { Contact, ContactService } from './contact.service';
import { HttpErrorResponse } from '@angular/common/http';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Component, effect } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { take } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import {
DialogBottomSheetContent,
DialogBottomSheetWrapper,
} from 'src/app/shared/components/dialogBottomSheet.component';
enum Operations {
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);
},
});
}
}
Contact,
ContactService,
ViewReadyContact,
contactTypeToViewReadyContact,
} from './contact.service';
@Component({
selector: 'app-contact',
@@ -115,61 +30,67 @@ export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
})
export default class ContactComponent {
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')
detailsComponentWrapper!: DialogBottomSheetWrapper;
dataSource: MatTableDataSource<ViewReadyContact> =
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(
public contactService: ContactService,
private _snackBar: MatSnackBar
private registrarService: RegistrarService
) {
// TODO: Refactor to registrarId service
this.loading = true;
this.contactService.fetchContacts().subscribe(() => {
this.loading = false;
effect(() => {
if (this.registrarService.registrarId()) {
this.contactService.isContactDetailsView = false;
this.contactService.isContactNewView = false;
this.contactService
.fetchContacts()
.pipe(take(1))
.subscribe((contacts) => {
this.dataSource = new MatTableDataSource<ViewReadyContact>(
contacts.map(contactTypeToViewReadyContact)
);
});
}
});
}
deleteContact(contact: Contact) {
if (confirm(`Please confirm contact ${contact.name} delete`)) {
this.contactService.deleteContact(contact).subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
}
openDetails(contact: Contact) {
this.contactService.setEditableContact(contact);
this.contactService.isContactDetailsView = true;
}
openCreateNew(e: MouseEvent) {
const newContact: Contact = {
name: '',
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 }
);
openNewContact() {
this.contactService.setEditableContact();
this.contactService.isContactNewView = true;
}
}
@@ -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");
// you may not use this file except in compliance with the License.
@@ -13,65 +13,112 @@
// limitations under the License.
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 { 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 {
name: string;
phoneNumber: string;
phoneNumber?: string;
emailAddress: string;
registrarId?: string;
faxNumber?: string;
types: Array<string>;
types: Array<contactType>;
visibleInWhoisAsAdmin?: boolean;
visibleInWhoisAsTech?: 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({
providedIn: 'root',
})
export class ContactService {
contacts = signal<Contact[]>([]);
contacts = signal<ViewReadyContact[]>([]);
contactInEdit!: ViewReadyContact;
isContactDetailsView: boolean = false;
isContactNewView: boolean = false;
constructor(
private backend: BackendService,
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[]> {
return this.backend.getContacts(this.registrarService.registrarId()).pipe(
tap((contacts = []) => {
this.contacts.set(contacts);
tap((contacts) => {
this.contacts.set(contacts.map(contactTypeToViewReadyContact));
})
);
}
saveContacts(contacts: Contact[]): Observable<Contact[]> {
saveContacts(contacts: ViewReadyContact[]): Observable<Contact[]> {
return this.backend
.postContacts(this.registrarService.registrarId(), contacts)
.pipe(
tap((_) => {
this.contacts.set(contacts);
})
);
.pipe(switchMap((_) => this.fetchContacts()));
}
updateContact(index: number, contact: Contact) {
updateContact(index: number, contact: ViewReadyContact) {
const newContacts = this.contacts().map((c, i) =>
i === index ? contact : c
);
return this.saveContacts(newContacts);
}
addContact(contact: Contact) {
addContact(contact: ViewReadyContact) {
const newContacts = this.contacts().concat([contact]);
return this.saveContacts(newContacts);
}
deleteContact(contact: Contact) {
deleteContact(contact: ViewReadyContact) {
const newContacts = this.contacts().filter((c) => c !== contact);
return this.saveContacts(newContacts);
}
@@ -1,104 +1,201 @@
<h3 mat-dialog-title>Contact details</h3>
<div mat-dialog-content *ngIf="contact">
<form (ngSubmit)="saveAndClose($event)">
<p>
<mat-form-field class="contact-details__input">
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
<div class="console-app__contact-controls">
<button
mat-icon-button
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>
<input
matInput
[required]="true"
[(ngModel)]="contact.name"
[(ngModel)]="contactService.contactInEdit.name"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</p>
<p>
<mat-form-field class="contact-details__input">
<mat-form-field appearance="outline">
<mat-label>Primary account email: </mat-label>
<input
type="email"
matInput
[email]="true"
[required]="true"
[(ngModel)]="contact.emailAddress"
[(ngModel)]="contactService.contactInEdit.emailAddress"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</p>
<p>
<mat-form-field class="contact-details__input">
<mat-form-field appearance="outline">
<mat-label>Phone: </mat-label>
<input
matInput
[(ngModel)]="contact.phoneNumber"
[(ngModel)]="contactService.contactInEdit.phoneNumber"
[ngModelOptions]="{ standalone: true }"
placeholder="+0.0000000000"
/>
</mat-form-field>
</p>
<p>
<mat-form-field class="contact-details__input">
<mat-form-field appearance="outline">
<mat-label>Fax: </mat-label>
<input
matInput
[(ngModel)]="contact.faxNumber"
[(ngModel)]="contactService.contactInEdit.faxNumber"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</p>
</section>
<div class="contact-details__group">
<label>Contact type:</label>
<div class="contact-details__group-content">
<section>
<h1>Contact Type</h1>
<p class="console-app__contact-required">
<mat-icon color="accent">error</mat-icon>Required to select at least one
</p>
<div class="">
<mat-checkbox
*ngFor="let contactType of contactTypes"
[checked]="contact.types.includes(contactType.value)"
(change)="
$event.checked
? contact.types.push(contactType.value)
: contact.types.splice(
contact.types.indexOf(contactType.value),
1
)
"
[disabled]="
contact.types.length === 1 && contact.types[0] === contactType.value
"
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.label }}
{{ contactType.value }}
</mat-checkbox>
</div>
</div>
<section>
<mat-checkbox
[(ngModel)]="contact.visibleInWhoisAsAdmin"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as admin contact</mat-checkbox
>
</section>
<section>
<mat-checkbox
[(ngModel)]="contact.visibleInWhoisAsTech"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as technical contact</mat-checkbox
>
</section>
<h1>WHOIS Preferences</h1>
<div>
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as admin contact</mat-checkbox
>
</div>
<section>
<mat-checkbox
[(ngModel)]="contact.visibleInDomainWhoisAsAbuse"
[ngModelOptions]="{ standalone: true }"
>Show Phone and Email in Domain WHOIS Record as registrar abuse contact
(per CL&D requirements)</mat-checkbox
>
<div>
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as technical contact</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>
<mat-dialog-actions>
<button mat-button (click)="close()">Cancel</button>
<button type="submit" mat-button>Save</button>
</mat-dialog-actions>
<button
class="console-app__contact-submit"
mat-flat-button
color="primary"
type="submit"
>
Save
</button>
</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>
@@ -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">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="settings-security" *ngIf="!loading">
<div class="settings-security__section">
<div class="settings-security__section-description">
<h2>IP Allowlist</h2>
<p>
Restrict access to EPP production servers to the following IP/IPv6
addresses, or ranges like 1.1.1.0/24
</p>
</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"
/>
@if(securityService.isEditingSecurity) {
<app-security-edit></app-security-edit>
} @else {
<div class="settings-security">
<mat-card appearance="outlined">
<mat-card-content>
<mat-list role="list">
<!-- IP Allowlist Start -->
<mat-list-item role="listitem">
<div class="settings-security__section-header">
<h2>IP Allowlist</h2>
<button
*ngIf="inEdit"
matSuffix
mat-icon-button
aria-label="Remove"
(click)="removeIpEntry(index)"
mat-flat-button
color="primary"
aria-label="Edit Contact"
(click)="editSecurity()"
>
<mat-icon>close</mat-icon>
<mat-icon>edit</mat-icon>
Edit
</button>
</mat-form-field>
</div>
</div>
<button mat-stroked-button (click)="enableEdit(); createIpEntry()">
Add IP
</button>
</div>
</div>
<div class="settings-security__section">
<div class="settings-security__section-description">
<h2>SSL Certificate</h2>
<p>X.509 PEM certificate for EPP production access.</p>
</div>
<div class="settings-security__section-form">
<textarea
matInput
class="settings-security__clientCertificate"
[(ngModel)]="dataSource.clientCertificate"
[disabled]="!inEdit"
></textarea>
</div>
</div>
<div class="settings-security__section">
<div class="settings-security__section-description">
<h2>Failover SSL Certificate</h2>
<p>X.509 PEM backup certificate for EPP Production Access.</p>
</div>
<div class="settings-security__section-form">
<textarea
matInput
[(ngModel)]="dataSource.failoverClientCertificate"
[disabled]="!inEdit"
></textarea>
</div>
</div>
<div class="settings-security__actions">
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
<button
class="settings-security__actions-save"
mat-raised-button
color="primary"
(click)="save()"
>
Save
</button>
<button
class="settings-security__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-list-item>
<mat-list-item role="listitem" lines="3">
<span class="console-app__list-value"
>Restrict access to EPP production servers to the following IP/IPv6
addresses, or ranges like 1.1.1.0/24</span
>
</mat-list-item>
<mat-divider></mat-divider>
@for (item of dataSource.ipAddressAllowList; track item.value) {
<mat-list-item role="listitem">
<span class="console-app__list-value">{{ item.value }}</span>
</mat-list-item>
<mat-divider></mat-divider>
} @empty {
<mat-list-item role="listitem">
<span class="console-app__list-value">No IP addresses on file.</span>
</mat-list-item>
}
<mat-list-item role="listitem"></mat-list-item>
<!-- IP Allowlist End -->
<!-- SSL Certificate Start -->
<mat-list-item role="listitem">
<h2>SSL Certificate</h2>
</mat-list-item>
<mat-list-item role="listitem">
<span class="console-app__list-value"
>X.509 PEM certificate for EPP production access</span
>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem" lines="10">
<span class="console-app__list-value">{{
dataSource.clientCertificate || "No client certificate on file."
}}</span>
</mat-list-item>
<mat-list-item role="listitem"> </mat-list-item>
<!-- SSL Certificate End -->
<!-- Failover SSL Certificate Start -->
<mat-list-item role="listitem">
<h2>Failover SSL Certificate</h2>
</mat-list-item>
<mat-list-item role="listitem">
<span class="console-app__list-value"
>X.509 PEM backup certificate for EPP production access</span
>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem" lines="10">
<span class="console-app__list-value">{{
dataSource.failoverClientCertificate ||
"No failover certificate on file."
}}</span>
</mat-list-item>
<!-- Failover SSL Certificate End -->
</mat-list>
</mat-card-content>
</mat-card>
</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");
// you may not use this file except in compliance with the License.
@@ -13,41 +13,14 @@
// limitations under the License.
.settings-security {
margin-top: 1.5rem;
h1 {
margin: 0;
max-width: 616px;
mat-card {
margin-bottom: 40px;
}
&__ipRecord {
margin-bottom: 1rem;
}
&__section {
&__section-header {
display: flex;
align-items: stretch;
margin-bottom: 3rem;
flex-wrap: wrap;
}
&__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;
justify-content: space-between;
align-items: center;
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");
// 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");
// 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
// limitations under the License.
import { Component } from '@angular/core';
import { Component, effect } from '@angular/core';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import {
SecurityService,
SecuritySettings,
apiToUiConverter,
} 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({
selector: 'app-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.scss'],
providers: [SecurityService],
})
export default class SecurityComponent {
public static PATH = 'security';
loading: boolean = false;
inEdit: boolean = false;
dataSource: SecuritySettings = {};
constructor(
public securityService: SecurityService,
private _snackBar: MatSnackBar,
public registrarService: RegistrarService
) {
this.dataSource = apiToUiConverter(this.registrarService.registrar());
}
enableEdit() {
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);
},
effect(() => {
if (this.registrarService.registrar()) {
this.dataSource = apiToUiConverter(this.registrarService.registrar());
this.securityService.isEditingSecurity = false;
}
});
this.cancel();
}
removeIpEntry(index: number) {
this.dataSource.ipAddressAllowList =
this.dataSource.ipAddressAllowList?.filter((_, i) => i != index);
}
resetDataSource() {
this.dataSource = apiToUiConverter(this.registrarService.registrar());
editSecurity() {
this.securityService.isEditingSecurity = true;
}
}
@@ -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");
// 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");
// 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 { BackendService } from 'src/app/shared/services/backend.service';
interface ipAllowListItem {
export interface ipAllowListItem {
value: string;
}
export interface SecuritySettings {
@@ -52,9 +52,12 @@ export function uiToApiConverter(
});
}
@Injectable()
@Injectable({
providedIn: 'root',
})
export class SecurityService {
securitySettings: SecuritySettings = {};
isEditingSecurity: boolean = false;
constructor(
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>
@@ -0,0 +1,33 @@
// 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.
.settings-security__edit {
max-width: 616px;
h1 {
margin-top: 30px;
}
mat-form-field {
width: 100%;
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">
<h1>Settings</h1>
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link routerLink="contact" routerLinkActive="active-link"
>Contact Info</a
<app-selected-registrar-wrapper>
<div class="console-settings">
<h1 class="mat-headline-4">Settings</h1>
<nav
mat-tab-nav-bar
mat-stretch-tabs="false"
mat-align-tabs="start"
[tabPanel]="tabPanel"
>
<a mat-tab-link routerLink="whois" routerLinkActive="active-link"
>WHOIS Info</a
>
<a mat-tab-link routerLink="security" routerLinkActive="active-link"
>Security</a
>
<a mat-tab-link routerLink="epp-password" routerLinkActive="active-link"
>EPP Password</a
>
<a mat-tab-link routerLink="users" routerLinkActive="active-link">Users</a>
<a mat-tab-link routerLink="registrars" routerLinkActive="active-link"
>Registrars</a
>
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
<a
mat-tab-link
routerLink="contact"
routerLinkActive="active-link"
queryParamsHandling="merge"
>Contacts</a
>
<a
mat-tab-link
routerLink="whois"
routerLinkActive="active-link"
queryParamsHandling="merge"
>WHOIS Info</a
>
<a
mat-tab-link
routerLink="security"
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");
// you may not use this file except in compliance with the License.
@@ -13,6 +13,9 @@
// limitations under the License.
.console-settings {
> mat-divider {
margin-bottom: 40px;
}
.mdc-tab {
&.active-link {
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");
// 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");
// 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 {
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");
// 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");
// 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");
// you may not use this file except in compliance with the License.
@@ -1,244 +1,105 @@
<div class="settings-whois">
<h2>WHOIS settings</h2>
<h3>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
</h3>
<div *ngIf="loading" class="settings-whois__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Name:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.registrarName"
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>
@if(whoisService.editing) {
<app-whois-edit></app-whois-edit>
} @else {
<div class="console-app__whois">
<div class="console-app__whois-controls">
<span>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
</span>
<div class="spacer"></div>
<button
mat-flat-button
color="primary"
aria-label="Edit WHOIS record"
(click)="whoisService.editing = true"
>
<mat-icon>edit</mat-icon>
Edit
</button>
</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>
}
@@ -1,59 +1,17 @@
// 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.
.console-app__whois {
max-width: 616px;
.settings-whois {
margin-top: 1.5rem;
&__section {
&-controls {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
min-width: 400px;
}
&__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;
align-items: center;
gap: 1rem;
margin: 20px 0;
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");
// 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");
// 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
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { Component, computed } from '@angular/core';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { WhoisService } from './whois.service';
@@ -26,51 +21,29 @@ import { WhoisService } from './whois.service';
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
providers: [WhoisService],
})
export default class WhoisComponent {
public static PATH = 'whois';
loading = false;
inEdit = false;
registrar: Registrar;
formattedAddress = computed(() => {
let result = '';
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(
public whoisService: WhoisService,
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)
);
}
public 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");
// you may not use this file except in compliance with the License.
@@ -14,21 +14,17 @@
import { Injectable } from '@angular/core';
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';
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail?: string;
localizedAddress?: Address;
registrarId?: string;
url?: string;
whoisServer?: string;
}
@Injectable()
@Injectable({
providedIn: 'root',
})
export class WhoisService {
whoisRegistrarFields: WhoisRegistrarFields = {};
editing: boolean = false;
constructor(
private backend: BackendService,
@@ -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;
}
}
@@ -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);
},
});
}
}