Commit 16df9fec authored by Steven Zou's avatar Steven Zou Committed by GitHub
Browse files

Merge pull request #1437 from vmware/fix/user_profile_dropdown

Implement user list and fix some related issues
parents 6c23d1ad 23f0e3dd
......@@ -55,4 +55,4 @@
"typings": "^1.4.0",
"webdriver-manager": "10.2.5"
}
}
}
\ No newline at end of file
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
<h3 class="modal-title">Accout Settings</h3>
<div class="modal-body">
<h3 class="modal-title">User Profile</h3>
<div class="modal-body" style="overflow-y: hidden;">
<form #accountSettingsFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
......@@ -29,11 +29,17 @@
</div>
<div class="form-group">
<label for="account_settings_comments" class="col-md-4">Comments</label>
<input type="text" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="51">
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="48">
<span class="tooltip-content">
Length of comment should be less than 20
</span>
</label>
</div>
</section>
</form>
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [hidden]='errorMessage === ""'>
<div style="height: 30px;"></div>
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [(clrAlertClosed)]="alertClose">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
......
......@@ -14,6 +14,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
staticBackdrop: boolean = true;
account: SessionUser;
error: any;
alertClose: boolean = true;
private isOnCalling: boolean = false;
private formValueChanged: boolean = false;
......@@ -37,7 +38,16 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
public get errorMessage(): string {
return this.error ? (this.error.message ? this.error.message : this.error) : "";
if(this.error){
if(this.error.message){
return this.error.message;
}else{
if(this.error._body){
return this.error._body;
}
}
}
return "";
}
ngAfterViewChecked(): void {
......@@ -85,7 +95,8 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
})
.catch(error => {
this.isOnCalling = false;
this.error = error
this.error = error;
this.alertClose = false;
});
}
......
......@@ -4,6 +4,7 @@ import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
import { ProjectComponent } from '../project/project.component';
import { UserComponent } from '../user/user.component';
import { BaseRoutingResolver } from './base-routing-resolver.service';
......@@ -19,6 +20,13 @@ const baseRoutes: Routes = [
{
path: 'projects',
component: ProjectComponent
},
{
path: 'users',
component: UserComponent,
resolve: {
projectsResolver: BaseRoutingResolver
}
}
]
}];
......
......@@ -16,7 +16,7 @@
<input id="tabsystem" type="checkbox">
<label for="tabsystem">System Managements</label>
<ul class="nav-list">
<li><a class="nav-link">Users</a></li>
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">Users</a></li>
<li><a class="nav-link">Replications</a></li>
<li><a class="nav-link">Quarantine[*]</a></li>
<li><a class="nav-link">Configurations[*]</a></li>
......
......@@ -12,10 +12,22 @@
<span class="custom-divider"></span>
<a href="javascript:void(0)" class="nav-link nav-text sign-up-override" routerLink="/sign-up" routerLinkActive="active">Sign Up</a>
</div>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
<clr-icon shape="world" style="left:-5px;"></clr-icon>
<span>English</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>English</a>
<a href="javascript:void(0)" clrDropdownItem>中文简体</a>
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
</div>
</clr-dropdown>
<clr-dropdown [clrMenuPosition]="'bottom-left'" class="dropdown" *ngIf="isSessionValid">
<button class="nav-text" clrDropdownToggle>
<clr-icon shape="user" class="is-inverse" size="24" style="left: -2px;"></clr-icon>
<span>Administrator</span>
<span>{{accountName}}</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
......@@ -26,17 +38,5 @@
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">Log out</a>
</div>
</clr-dropdown>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
<clr-icon shape="world" style="left:-5px;"></clr-icon>
<span>English</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>English</a>
<a href="javascript:void(0)" clrDropdownItem>中文简体</a>
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
</div>
</clr-dropdown>
</div>
</clr-header>
\ No newline at end of file
......@@ -32,6 +32,10 @@ export class NavigatorComponent implements OnInit {
return this.sessionUser != null;
}
public get accountName(): string {
return this.sessionUser?this.sessionUser.username: "";
}
//Open the account setting dialog
openAccountSettingsModal(): void {
this.showAccountSettingsModal.emit({
......
<span style="float: right; margin-right: 24px;">
<clr-dropdown [clrMenuPosition]="'bottom-right'" [clrCloseMenuOnItemClick]="true" style="position: absolute;">
<button clrDropdownToggle>
<clr-icon shape="ellipses-vertical"></clr-icon>
</button>
<div class="dropdown-menu">
<ng-content></ng-content>
</div>
</clr-dropdown>
</span>
\ No newline at end of file
import {Component} from "@angular/core";
@Component({
selector: "clr-dg-action-overflow",
templateUrl: "datagrid-action-overflow.html"
})
export class DatagridActionOverflow {
}
\ No newline at end of file
.filter-icon {
position: relative;
right: -12px;
}
\ No newline at end of file
<span>
<clr-icon shape="filter" size="12" class="is-solid filter-icon"></clr-icon>
<input type="text" style="padding-left: 15px;" (keyup)="valueChange()" placeholder="{{placeHolder}}" [(ngModel)]="currentValue"/>
</span>
\ No newline at end of file
import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@Component({
selector: 'grid-filter',
templateUrl: 'filter.component.html',
styleUrls: ['filter.component.css']
})
export class FilterComponent implements OnInit{
private placeHolder: string = "";
private currentValue: string = "";
private leadingSpacesAdded: boolean = false;
private filerAction: Function;
private filterTerms = new Subject<string>();
@Output("filter") private filterEvt = new EventEmitter<string>();
@Input("filterPlaceholder")
public set flPlaceholder(placeHolder: string) {
this.placeHolder = placeHolder;
}
ngOnInit(): void {
this.filterTerms
.debounceTime(300)
.distinctUntilChanged()
.subscribe(terms => {
this.filterEvt.emit(terms);
});
}
valueChange(): void {
//Send out filter terms
this.filterTerms.next(this.currentValue.trim());
}
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ export function maxLengthExtValidator(length: number): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
const value: string = control.value
if (!value || value.trim() === "") {
return { 'maxLengthExt': 0 };
return null;
}
const regExp = new RegExp(assiiChars, 'i');
......@@ -42,7 +42,6 @@ export class MaxLengthExtValidatorDirective implements Validator, OnChanges {
} else {
this.valFn = Validators.nullValidator;
}
console.info(changes, this.maxLengthExt);
}
validate(control: AbstractControl): { [key: string]: any } {
......
......@@ -6,21 +6,25 @@ import { SessionService } from '../shared/session.service';
import { MessageComponent } from '../global-message/message.component';
import { MessageService } from '../global-message/message.service';
import { MaxLengthExtValidatorDirective } from './max-length-ext.directive';
import { FilterComponent } from './filter/filter.component';
import { DatagridActionOverflow } from './clg-dg-action-overflow/datagrid-action-overflow';
@NgModule({
imports: [
CoreModule,
//AccountModule
CoreModule
],
declarations: [
MessageComponent,
MaxLengthExtValidatorDirective
MaxLengthExtValidatorDirective,
FilterComponent,
DatagridActionOverflow
],
exports: [
CoreModule,
// AccountModule,
MessageComponent,
MaxLengthExtValidatorDirective
MaxLengthExtValidatorDirective,
FilterComponent,
DatagridActionOverflow
],
providers: [SessionService, MessageService]
})
......
<div>
<form #newUserFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="username" class="col-md-4 required">Username</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="usernameInput.invalid && (usernameInput.dirty || usernameInput.touched)">
<input type="text" required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="46">
<span class="tooltip-content">
Username is required and should match the following rules:Not contain '"~#$%"' and length less than 20
</span>
</label>
</div>
<div class="form-group">
<label for="email" class="col-md-4 required">Email</label>
<label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
<input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email"
required
pattern='^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="46">
<span class="tooltip-content">
Email should be a valid email address like name@example.com
</span>
</label>
</div>
<div class="form-group">
<label for="realname" class="col-md-4 required">Full name</label>
<label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="fullNameInput.invalid && (fullNameInput.dirty || fullNameInput.touched)">
<input type="text" name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="46">
<span class="tooltip-content">
Max length of full name is 20
</span>
</label>
</div>
<div class="form-group">
<label for="newPassword" class="required">New Password</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
<input type="password" id="newPassword" placeholder="Enter new password"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="newPassword"
[(ngModel)]="newUser.password"
#newPassInput="ngModel" size="46">
<span class="tooltip-content">
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number
</span>
</label>
</div>
<div class="form-group">
<label for="confirmPassword" class="required">Confirm Password</label>
<label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="(confirmPassInput.invalid && (confirmPassInput.dirty || confirmPassInput.touched)) || (!confirmPassInput.invalid && confirmPassInput.value != newPassInput.value)">
<input type="password" id="confirmPassword" placeholder="Confirm new password"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="confirmPassword"
[(ngModel)]="confirmedPwd"
#confirmPassInput="ngModel" size="46">
<span class="tooltip-content">
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number and same with new password
</span>
</label>
</div>
<div class="form-group">
<label for="comment" class="col-md-4">Comments</label>
<label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
<input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="46">
<span class="tooltip-content">
Length of comment should be less than 20
</span>
</label>
</div>
</section>
</form>
</div>
\ No newline at end of file
import { Component, ViewChild, AfterViewChecked, Output, EventEmitter } from '@angular/core';
import { NgForm } from '@angular/forms';
import { User } from './user';
@Component({
selector: 'new-user-form',
templateUrl: 'new-user-form.component.html'
})
export class NewUserFormComponent implements AfterViewChecked {
newUser: User = new User();
confirmedPwd: string = "";
newUserFormRef: NgForm;
@ViewChild("newUserFrom") newUserForm: NgForm;
//Notify the form value changes
@Output() valueChange = new EventEmitter<boolean>();
public get isValid(): boolean {
let pwdEqualStatus = true;
if(this.newUserForm.controls["confirmPassword"] &&
this.newUserForm.controls["newPassword"]){
pwdEqualStatus = this.newUserForm.controls["confirmPassword"].value === this.newUserForm.controls["newPassword"].value;
}
return this.newUserForm &&
this.newUserForm.valid && pwdEqualStatus;
}
ngAfterViewChecked(): void {
if (this.newUserFormRef != this.newUserForm) {
this.newUserFormRef = this.newUserForm;
if (this.newUserFormRef) {
this.newUserFormRef.valueChanges.subscribe(data => {
this.valueChange.emit(true);
});
}
}
}
//Return the current user data
getData(): User {
return this.newUser;
}
reset(): void {
if(this.newUserForm){
this.newUserForm.reset();
}
}
}
\ No newline at end of file
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
<h3 class="modal-title">Add User</h3>
<div class="modal-body">
<new-user-form (valueChange)="formValueChange($event)"></new-user-form>
<div style="height: 30px;"></div>
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [(clrAlertClosed)]="alertClose">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="inProgress === false"> </span>
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">Ok</button>
</div>
</clr-modal>
\ No newline at end of file
import { Component, ViewChild, Output,EventEmitter } from '@angular/core';
import { NgForm } from '@angular/forms';
import { NewUserFormComponent } from './new-user-form.component';
import { User } from './user';
import { SessionService } from '../shared/session.service';
import { UserService } from './user.service';
@Component({
selector: "new-user-modal",
templateUrl: "new-user-modal.component.html"
})
export class NewUserModalComponent {
opened: boolean = false;
alertClose: boolean = true;
private error: any;
private onGoing: boolean = false;
@Output() addNew = new EventEmitter<User>();
constructor(private session: SessionService,
private userService: UserService) { }
@ViewChild(NewUserFormComponent)
private newUserForm: NewUserFormComponent;
private getNewUser(): User {
return this.newUserForm.getData();
}
public get inProgress(): boolean {
return this.onGoing;
}
public get isValid(): boolean {
return this.newUserForm.isValid;
}
public get errorMessage(): string {
if (this.error) {
if (this.error.message) {
return this.error.message;
} else {
if (this.error._body) {
return this.error._body;
}
}
}
return "";
}
formValueChange(flag: boolean): void {
if (!this.alertClose) {
this.alertClose = true;//If alert is shown, then close it
}
}
open(): void {
this.opened = true;
}
close(): void {
this.newUserForm.reset();//Reset form
this.opened = false;
}
//Create new user
create(): void {
//Double confirm everything is ok
//Form is valid
if(!this.isValid){
return;
}
//We have new user data
let u = this.getNewUser();
if(!u){
return;
}
//Session is ok and role is matched
let account = this.session.getCurrentUser();
if(!account || account.has_admin_role === 0){
return;
}
//Start process
this.onGoing = true;
this.userService.addUser(u)
.then(() => {
this.onGoing = false;
//TODO:
//As no response data returned, can not add it to list directly
this.addNew.emit(u);
this.close();
})
.catch(error => {
this.onGoing = false;
this.error = error;
});
}
}
\ No newline at end of file
.custom-h2 {
margin-top: 0px !important;
}
.custom-add-button {
font-size: medium;
margin-left: -12px;
}
.filter-icon {
position: relative;
right: -12px;
}
.filter-pos {
float: right;
margin-right: 24px;
position: relative;
top: 8px;
}
.action-panel-pos {
position: relative;
top: 20px;
}
.refresh-btn {
position: absolute;
right: -4px;
top: 8px;
cursor: pointer;
}
\ No newline at end of file
<div>
<h2 class="custom-h2">Users</h2>
<div class="action-panel-pos">
<span>
<clr-icon shape="plus" class="is-highlight" size="24"></clr-icon>
<button type="submit" class="btn btn-link custom-add-button" (click)="addNewUser()">USER</button>
</span>
<grid-filter class="filter-pos" filterPlaceholder="Filter users" (filter)="doFilter($event)"></grid-filter>
<span class="refresh-btn" (click)="refreshUser()">