mirror of
https://github.com/google/nomulus
synced 2026-01-05 04:56:03 +00:00
Allow adding existing users to registrar (#2616)
This commit is contained in:
@@ -166,9 +166,9 @@ export class BackendService {
|
||||
.pipe(catchError((err) => this.errorCatcher<User[]>(err)));
|
||||
}
|
||||
|
||||
createUser(registrarId: string): Observable<User> {
|
||||
createUser(registrarId: string, maybeUser: User | null): Observable<User> {
|
||||
return this.http
|
||||
.post<User>(`/console-api/users?registrarId=${registrarId}`, {})
|
||||
.post<User>(`/console-api/users?registrarId=${registrarId}`, maybeUser)
|
||||
.pipe(catchError((err) => this.errorCatcher<User>(err)));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface UserData {
|
||||
supportEmail: string;
|
||||
supportPhoneNumber: string;
|
||||
technicalDocsUrl: string;
|
||||
userRoles?: Map<string, string>;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SelectedRegistrarModule } from '../app.module';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
import { User, UsersService, roleToDescription } from './users.service';
|
||||
import { UsersService, roleToDescription } from './users.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
<div class="console-app__users-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
} @else if(selectingExistingUser) {
|
||||
|
||||
<div class="console-app__users">
|
||||
<h1 class="mat-headline-4">Add existing user</h1>
|
||||
|
||||
<p>
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to users list"
|
||||
(click)="selectingExistingUser = false"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
<h1>Select registrar from which to add a new user</h1>
|
||||
<p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar</mat-label>
|
||||
<mat-select
|
||||
[(ngModel)]="selectedRegistrarId"
|
||||
name="selectedRegistrarId"
|
||||
(selectionChange)="onRegistrarSelectionChange($event)"
|
||||
>
|
||||
@for (registrar of registrarService.registrars(); track registrar) {
|
||||
<mat-option [value]="registrar.registrarId">{{
|
||||
registrar.registrarId
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
@if(usersSelection.length) {
|
||||
<app-users-list
|
||||
[users]="usersSelection"
|
||||
(onSelect)="existingUserSelected($event)"
|
||||
/>
|
||||
<p class="console-app__users-add-existing">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Add user"
|
||||
(click)="submitExistingUser()"
|
||||
[disabled]="!selectedExistingUser"
|
||||
>
|
||||
Add user
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
aria-label="Cancel adding existing user"
|
||||
(click)="selectingExistingUser = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
} @else if(usersService.currentlyOpenUserEmail()) {
|
||||
<app-user-edit></app-user-edit>
|
||||
} @else {
|
||||
@@ -10,39 +66,31 @@
|
||||
<div class="console-app__users-header">
|
||||
<h1 class="mat-headline-4">Users</h1>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="createNewUser()"
|
||||
aria-label="Create new user"
|
||||
color="primary"
|
||||
>
|
||||
Create a Viewer User
|
||||
</button>
|
||||
<div class="console-app__users-header-buttons">
|
||||
<button
|
||||
class="console-app__users-header-add"
|
||||
mat-stroked-button
|
||||
(click)="addExistingUser()"
|
||||
aria-label="Create new user"
|
||||
color="primary"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
Add existing user
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="createNewUser()"
|
||||
aria-label="Create new user"
|
||||
color="primary"
|
||||
>
|
||||
Create a Viewer User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__users-table"
|
||||
matSort
|
||||
>
|
||||
<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.emailAddress)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
<app-users-list
|
||||
[users]="usersService.users()"
|
||||
(onSelect)="openDetails($event)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</app-selected-registrar-wrapper>
|
||||
|
||||
@@ -13,26 +13,37 @@
|
||||
// limitations under the License.
|
||||
|
||||
.console-app {
|
||||
&__users {
|
||||
max-width: 1024px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&__users-spinner {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
$min-width: 756px;
|
||||
$max-width: 1024px;
|
||||
|
||||
&__users-table {
|
||||
min-width: $min-width !important;
|
||||
max-width: $max-width;
|
||||
}
|
||||
|
||||
&__users-new {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&__users-add-existing {
|
||||
margin-top: 20px;
|
||||
> button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
&__users-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
margin: 0 15px 15px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,29 +14,18 @@
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, effect, ViewChild } from '@angular/core';
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { SelectedRegistrarModule } from '../app.module';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
import { UserEditComponent } from './userEdit.component';
|
||||
import { roleToDescription, User, UsersService } from './users.service';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
columnDef: 'emailAddress',
|
||||
header: 'User email',
|
||||
cell: (record: User) => `${record.emailAddress || ''}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'role',
|
||||
header: 'User role',
|
||||
cell: (record: User) => `${roleToDescription(record.role)}`,
|
||||
},
|
||||
];
|
||||
import { User, UsersService } from './users.service';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UsersListComponent } from './usersList.component';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-users',
|
||||
@@ -44,41 +33,45 @@ export const columns = [
|
||||
styleUrls: ['./users.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
CommonModule,
|
||||
SelectedRegistrarModule,
|
||||
UsersListComponent,
|
||||
UserEditComponent,
|
||||
],
|
||||
providers: [UsersService],
|
||||
})
|
||||
export class UsersComponent {
|
||||
dataSource: MatTableDataSource<User>;
|
||||
columns = columns;
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
isLoading = false;
|
||||
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
selectingExistingUser = false;
|
||||
selectedRegistrarId = '';
|
||||
usersSelection: User[] = [];
|
||||
selectedExistingUser: User | undefined;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected usersService: UsersService,
|
||||
private userDataService: UserDataService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
this.dataSource = new MatTableDataSource<User>(usersService.users());
|
||||
|
||||
effect(() => {
|
||||
if (registrarService.registrarId()) {
|
||||
this.loadUsers();
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
this.dataSource.data = usersService.users();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.sort = this.sort;
|
||||
addExistingUser() {
|
||||
this.selectingExistingUser = true;
|
||||
this.selectedRegistrarId = '';
|
||||
this.usersSelection = [];
|
||||
this.selectedExistingUser = undefined;
|
||||
}
|
||||
|
||||
existingUserSelected(user: User) {
|
||||
this.selectedExistingUser = user;
|
||||
}
|
||||
|
||||
loadUsers() {
|
||||
@@ -96,7 +89,7 @@ export class UsersComponent {
|
||||
|
||||
createNewUser() {
|
||||
this.isLoading = true;
|
||||
this.usersService.createNewUser().subscribe({
|
||||
this.usersService.createOrAddNewUser(null).subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
this.isLoading = false;
|
||||
@@ -107,7 +100,39 @@ export class UsersComponent {
|
||||
});
|
||||
}
|
||||
|
||||
openDetails(emailAddress: string) {
|
||||
this.usersService.currentlyOpenUserEmail.set(emailAddress);
|
||||
openDetails(user: User) {
|
||||
this.usersService.currentlyOpenUserEmail.set(user.emailAddress);
|
||||
}
|
||||
|
||||
onRegistrarSelectionChange(e: MatSelectChange) {
|
||||
if (e.value) {
|
||||
this.usersService.fetchUsersForRegistrar(e.value).subscribe({
|
||||
error: (err) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
next: (users) => {
|
||||
this.usersSelection = users;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
submitExistingUser() {
|
||||
this.isLoading = true;
|
||||
if (this.selectedExistingUser) {
|
||||
this.usersService
|
||||
.createOrAddNewUser(this.selectedExistingUser)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
this.isLoading = false;
|
||||
},
|
||||
complete: () => {
|
||||
this.isLoading = false;
|
||||
this.selectingExistingUser = false;
|
||||
this.loadUsers();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ export class UsersService {
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
fetchUsersForRegistrar(registrarId: string) {
|
||||
return this.backendService.getUsers(registrarId);
|
||||
}
|
||||
|
||||
fetchUsers() {
|
||||
return this.backendService
|
||||
.getUsers(this.registrarService.registrarId())
|
||||
@@ -56,14 +60,16 @@ export class UsersService {
|
||||
);
|
||||
}
|
||||
|
||||
createNewUser() {
|
||||
createOrAddNewUser(maybeExistingUser: User | null) {
|
||||
return this.backendService
|
||||
.createUser(this.registrarService.registrarId())
|
||||
.createUser(this.registrarService.registrarId(), maybeExistingUser)
|
||||
.pipe(
|
||||
tap((newUser: User) => {
|
||||
this.users.set([...this.users(), newUser]);
|
||||
this.currentlyOpenUserEmail.set(newUser.emailAddress);
|
||||
this.isNewUser = true;
|
||||
if (newUser) {
|
||||
this.users.set([...this.users(), newUser]);
|
||||
this.currentlyOpenUserEmail.set(newUser.emailAddress);
|
||||
this.isNewUser = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
24
console-webapp/src/app/users/usersList.component.html
Normal file
24
console-webapp/src/app/users/usersList.component.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="console-app__users-table-wrapper">
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__users-table"
|
||||
matSort
|
||||
>
|
||||
<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"
|
||||
[class.rowSelected]="isRowSelected(row)"
|
||||
(click)="onClick(row)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
14
console-webapp/src/app/users/usersList.component.scss
Normal file
14
console-webapp/src/app/users/usersList.component.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.console-app {
|
||||
&__users-table {
|
||||
min-width: 616px;
|
||||
.rowSelected {
|
||||
background-color: var(--light-highlight);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__users-table-wrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
78
console-webapp/src/app/users/usersList.component.ts
Normal file
78
console-webapp/src/app/users/usersList.component.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
EventEmitter,
|
||||
input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { User, roleToDescription } from './users.service';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
columnDef: 'emailAddress',
|
||||
header: 'User email',
|
||||
cell: (record: User) => `${record.emailAddress || ''}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'role',
|
||||
header: 'User role',
|
||||
cell: (record: User) => `${roleToDescription(record.role)}`,
|
||||
},
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-users-list',
|
||||
templateUrl: './usersList.component.html',
|
||||
styleUrls: ['./usersList.component.scss'],
|
||||
standalone: true,
|
||||
imports: [MaterialModule, CommonModule],
|
||||
providers: [],
|
||||
})
|
||||
export class UsersListComponent {
|
||||
columns = columns;
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
dataSource: MatTableDataSource<User>;
|
||||
selectedRow!: User;
|
||||
users = input<User[]>([]);
|
||||
@Output() onSelect = new EventEmitter<User>();
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
constructor() {
|
||||
this.dataSource = new MatTableDataSource<User>(this.users());
|
||||
effect(() => {
|
||||
this.dataSource.data = this.users();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
onClick(row: User) {
|
||||
this.selectedRow = row;
|
||||
this.onSelect.emit(row);
|
||||
}
|
||||
|
||||
isRowSelected(row: User) {
|
||||
return row === this.selectedRow;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user