From 23f0e3dda58c8757bfc849b04f3723ba38c1142e Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Thu, 23 Feb 2017 23:58:44 +0800 Subject: [PATCH] Implement user list and fix some related issues --- harbor-app/package.json | 2 +- .../account-settings-modal.component.html | 14 +- .../account-settings-modal.component.ts | 15 ++- .../src/app/base/base-routing.module.ts | 8 ++ .../harbor-shell/harbor-shell.component.html | 2 +- .../base/navigator/navigator.component.html | 28 ++-- .../app/base/navigator/navigator.component.ts | 4 + .../datagrid-action-overflow.html | 10 ++ .../datagrid-action-overflow.ts | 9 ++ .../app/shared/filter/filter.component.css | 4 + .../app/shared/filter/filter.component.html | 4 + .../src/app/shared/filter/filter.component.ts | 43 +++++++ .../app/shared/max-length-ext.directive.ts | 3 +- harbor-app/src/app/shared/shared.module.ts | 14 +- .../src/app/user/new-user-form.component.html | 72 +++++++++++ .../src/app/user/new-user-form.component.ts | 52 ++++++++ .../app/user/new-user-modal.component.html | 19 +++ .../src/app/user/new-user-modal.component.ts | 106 ++++++++++++++++ harbor-app/src/app/user/user.component.css | 32 +++++ harbor-app/src/app/user/user.component.html | 37 ++++++ harbor-app/src/app/user/user.component.ts | 120 +++++++++++++++++- harbor-app/src/app/user/user.module.ts | 10 +- harbor-app/src/app/user/user.service.ts | 59 +++++++++ harbor-app/src/app/user/user.ts | 15 +++ 24 files changed, 648 insertions(+), 34 deletions(-) create mode 100644 harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.html create mode 100644 harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.ts create mode 100644 harbor-app/src/app/shared/filter/filter.component.css create mode 100644 harbor-app/src/app/shared/filter/filter.component.html create mode 100644 harbor-app/src/app/shared/filter/filter.component.ts create mode 100644 harbor-app/src/app/user/new-user-form.component.html create mode 100644 harbor-app/src/app/user/new-user-form.component.ts create mode 100644 harbor-app/src/app/user/new-user-modal.component.html create mode 100644 harbor-app/src/app/user/new-user-modal.component.ts create mode 100644 harbor-app/src/app/user/user.component.css create mode 100644 harbor-app/src/app/user/user.service.ts create mode 100644 harbor-app/src/app/user/user.ts diff --git a/harbor-app/package.json b/harbor-app/package.json index 928d90760..cf7068e87 100644 --- a/harbor-app/package.json +++ b/harbor-app/package.json @@ -55,4 +55,4 @@ "typings": "^1.4.0", "webdriver-manager": "10.2.5" } -} +} \ No newline at end of file diff --git a/harbor-app/src/app/account/account-settings/account-settings-modal.component.html b/harbor-app/src/app/account/account-settings/account-settings-modal.component.html index 5791eebc1..1fb92e62e 100644 --- a/harbor-app/src/app/account/account-settings/account-settings-modal.component.html +++ b/harbor-app/src/app/account/account-settings/account-settings-modal.component.html @@ -1,6 +1,6 @@ - - \ No newline at end of file diff --git a/harbor-app/src/app/base/navigator/navigator.component.ts b/harbor-app/src/app/base/navigator/navigator.component.ts index e8f48330a..95e05b6c4 100644 --- a/harbor-app/src/app/base/navigator/navigator.component.ts +++ b/harbor-app/src/app/base/navigator/navigator.component.ts @@ -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({ diff --git a/harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.html b/harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.html new file mode 100644 index 000000000..8a24fa978 --- /dev/null +++ b/harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.html @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.ts b/harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.ts new file mode 100644 index 000000000..dd24514bc --- /dev/null +++ b/harbor-app/src/app/shared/clg-dg-action-overflow/datagrid-action-overflow.ts @@ -0,0 +1,9 @@ +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 diff --git a/harbor-app/src/app/shared/filter/filter.component.css b/harbor-app/src/app/shared/filter/filter.component.css new file mode 100644 index 000000000..98d8eccf5 --- /dev/null +++ b/harbor-app/src/app/shared/filter/filter.component.css @@ -0,0 +1,4 @@ +.filter-icon { + position: relative; + right: -12px; +} \ No newline at end of file diff --git a/harbor-app/src/app/shared/filter/filter.component.html b/harbor-app/src/app/shared/filter/filter.component.html new file mode 100644 index 000000000..302b00362 --- /dev/null +++ b/harbor-app/src/app/shared/filter/filter.component.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/harbor-app/src/app/shared/filter/filter.component.ts b/harbor-app/src/app/shared/filter/filter.component.ts new file mode 100644 index 000000000..1c7061a89 --- /dev/null +++ b/harbor-app/src/app/shared/filter/filter.component.ts @@ -0,0 +1,43 @@ +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(); + + @Output("filter") private filterEvt = new EventEmitter(); + + @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 diff --git a/harbor-app/src/app/shared/max-length-ext.directive.ts b/harbor-app/src/app/shared/max-length-ext.directive.ts index d261c977a..f96e9e01f 100644 --- a/harbor-app/src/app/shared/max-length-ext.directive.ts +++ b/harbor-app/src/app/shared/max-length-ext.directive.ts @@ -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 } { diff --git a/harbor-app/src/app/shared/shared.module.ts b/harbor-app/src/app/shared/shared.module.ts index d82803aa3..119e11eea 100644 --- a/harbor-app/src/app/shared/shared.module.ts +++ b/harbor-app/src/app/shared/shared.module.ts @@ -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] }) diff --git a/harbor-app/src/app/user/new-user-form.component.html b/harbor-app/src/app/user/new-user-form.component.html new file mode 100644 index 000000000..d4f77f917 --- /dev/null +++ b/harbor-app/src/app/user/new-user-form.component.html @@ -0,0 +1,72 @@ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/harbor-app/src/app/user/new-user-form.component.ts b/harbor-app/src/app/user/new-user-form.component.ts new file mode 100644 index 000000000..1e47d6495 --- /dev/null +++ b/harbor-app/src/app/user/new-user-form.component.ts @@ -0,0 +1,52 @@ +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(); + + 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 diff --git a/harbor-app/src/app/user/new-user-modal.component.html b/harbor-app/src/app/user/new-user-modal.component.html new file mode 100644 index 000000000..9539d9ee7 --- /dev/null +++ b/harbor-app/src/app/user/new-user-modal.component.html @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/harbor-app/src/app/user/new-user-modal.component.ts b/harbor-app/src/app/user/new-user-modal.component.ts new file mode 100644 index 000000000..33186c938 --- /dev/null +++ b/harbor-app/src/app/user/new-user-modal.component.ts @@ -0,0 +1,106 @@ +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(); + + 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 diff --git a/harbor-app/src/app/user/user.component.css b/harbor-app/src/app/user/user.component.css new file mode 100644 index 000000000..eb5e65fee --- /dev/null +++ b/harbor-app/src/app/user/user.component.css @@ -0,0 +1,32 @@ +.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 diff --git a/harbor-app/src/app/user/user.component.html b/harbor-app/src/app/user/user.component.html index e69de29bb..3cdecab2e 100644 --- a/harbor-app/src/app/user/user.component.html +++ b/harbor-app/src/app/user/user.component.html @@ -0,0 +1,37 @@ +
+

Users

+
+ + + + + + + + + +
+
+ + Name + Administrator + Email + Registration time + + {{user.username}} + {{user.has_admin_role?"Yes":"No"}} + {{user.email}} + + {{user.creation_time}} + + {{user.has_admin_role?"Disable administrator":"Enable administrator"}} + + Delete + + + + {{users.length}} users + +
+ +
\ No newline at end of file diff --git a/harbor-app/src/app/user/user.component.ts b/harbor-app/src/app/user/user.component.ts index 8f600a0cf..f6fd35ab0 100644 --- a/harbor-app/src/app/user/user.component.ts +++ b/harbor-app/src/app/user/user.component.ts @@ -1,7 +1,121 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import 'rxjs/add/operator/toPromise'; + +import { UserService } from './user.service'; +import { User } from './user'; +import { NewUserModalComponent } from './new-user-modal.component'; @Component({ selector: 'harbor-user', - templateUrl: 'user.component.html' + templateUrl: 'user.component.html', + styleUrls: ['user.component.css'], + + providers: [UserService] }) -export class UserComponent {} \ No newline at end of file + +export class UserComponent implements OnInit { + users: User[] = []; + originalUsers: Promise; + private onGoing: boolean = false; + + @ViewChild(NewUserModalComponent) + private newUserDialog: NewUserModalComponent; + + constructor(private userService: UserService) { } + + private isMatchFilterTerm(terms: string, testedItem: string): boolean { + return testedItem.indexOf(terms) != -1; + } + + public get inProgress(): boolean { + return this.onGoing; + } + + ngOnInit(): void { + this.refreshUser(); + } + + //Filter items by keywords + doFilter(terms: string): void { + this.originalUsers.then(users => { + if (terms.trim() === "") { + this.users = users; + } else { + this.users = users.filter(user => { + return this.isMatchFilterTerm(terms, user.username); + }) + } + }); + } + + //Disable the admin role for the specified user + changeAdminRole(user: User): void { + //Double confirm user is existing + if (!user || user.user_id === 0) { + return; + } + + //Value copy + let updatedUser: User = Object.assign({}, user); + + if (updatedUser.has_admin_role === 0) { + updatedUser.has_admin_role = 1;//Set as admin + } else { + updatedUser.has_admin_role = 0;//Set as none admin + } + + this.userService.updateUser(updatedUser) + .then(() => { + //Change view now + user.has_admin_role = updatedUser.has_admin_role; + }) + .catch(error => console.error(error))//TODO: + } + + //Delete the specified user + deleteUser(userId: number): void { + if (userId === 0) { + return; + } + + this.userService.deleteUser(userId) + .then(() => { + //Remove it from current user list + //and then view refreshed + this.originalUsers.then(users => { + this.users = users.filter(user => user.user_id != userId); + }); + }) + .catch(error => console.error(error));//TODO: + } + + //Refresh the user list + refreshUser(): void { + //Start to get + this.onGoing = true; + + this.originalUsers = this.userService.getUsers() + .then(users => { + this.onGoing = false; + + this.users = users; + return users; + }) + .catch(error => { + this.onGoing = false; + console.error(error);//TODO: + }); + } + + //Add new user + addNewUser(): void { + this.newUserDialog.open(); + } + + //Add user to the user list + addUserToList(user: User): void { + //Currently we can only add it by reloading all + this.refreshUser(); + } + +} diff --git a/harbor-app/src/app/user/user.module.ts b/harbor-app/src/app/user/user.module.ts index 8d8b53040..8bc9ce570 100644 --- a/harbor-app/src/app/user/user.module.ts +++ b/harbor-app/src/app/user/user.module.ts @@ -1,15 +1,21 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { UserComponent } from './user.component'; +import { NewUserFormComponent } from './new-user-form.component'; +import { NewUserModalComponent } from './new-user-modal.component'; + @NgModule({ imports: [ SharedModule ], declarations: [ - UserComponent + UserComponent, + NewUserFormComponent, + NewUserModalComponent ], exports: [ - UserComponent + UserComponent, + NewUserFormComponent ] }) export class UserModule { diff --git a/harbor-app/src/app/user/user.service.ts b/harbor-app/src/app/user/user.service.ts new file mode 100644 index 000000000..c3e09ffe9 --- /dev/null +++ b/harbor-app/src/app/user/user.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { Headers, Http, RequestOptions } from '@angular/http'; +import 'rxjs/add/operator/toPromise'; + +import { User } from './user'; + +const userMgmtEndpoint = '/api/users'; + +/** + * Define related methods to handle account and session corresponding things + * + * @export + * @class SessionService + */ +@Injectable() +export class UserService { + private httpOptions = new RequestOptions({ + headers: new Headers({ + "Content-Type": 'application/json' + }) + }); + + constructor(private http: Http) { } + + //Handle the related exceptions + private handleError(error: any): Promise { + return Promise.reject(error.message || error); + } + + //Get the user list + getUsers(): Promise { + return this.http.get(userMgmtEndpoint, this.httpOptions).toPromise() + .then(response => response.json() as User[]) + .catch(error => this.handleError(error)); + } + + //Add new user + addUser(user: User): Promise { + return this.http.post(userMgmtEndpoint, JSON.stringify(user), this.httpOptions).toPromise() + .then(() => null) + .catch(error => this.handleError(error)); + } + + //Delete the specified user + deleteUser(userId: number): Promise { + return this.http.delete(userMgmtEndpoint+"/"+userId, this.httpOptions) + .toPromise() + .then(() => null) + .catch(error => this.handleError(error)); + } + + //Update user to enable/disable the admin role + updateUser(user: User): Promise { + return this.http.put(userMgmtEndpoint+"/"+user.user_id, JSON.stringify(user), this.httpOptions) + .toPromise() + .then(() => null) + .catch(error => this.handleError(error)); + } +} \ No newline at end of file diff --git a/harbor-app/src/app/user/user.ts b/harbor-app/src/app/user/user.ts new file mode 100644 index 000000000..f8c53fab7 --- /dev/null +++ b/harbor-app/src/app/user/user.ts @@ -0,0 +1,15 @@ +/** + * For user management + * + * @export + * @class User + */ +export class User { + user_id: number; + username: string; + email: string; + password?: string; + comment?: string; + has_admin_role: number; + creation_time: string; +} \ No newline at end of file