mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 20:26:13 +01:00
Merge pull request #1437 from vmware/fix/user_profile_dropdown
Implement user list and fix some related issues
This commit is contained in:
commit
16df9fec5d
@ -55,4 +55,4 @@
|
||||
"typings": "^1.4.0",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<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,20 +12,6 @@
|
||||
<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 [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>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">User Profile</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">Change Password</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<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>
|
||||
@ -38,5 +24,19 @@
|
||||
<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>{{accountName}}</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">User Profile</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">Change Password</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">Log out</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
</clr-header>
|
@ -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({
|
||||
|
@ -0,0 +1,10 @@
|
||||
<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>
|
@ -0,0 +1,9 @@
|
||||
import {Component} from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "clr-dg-action-overflow",
|
||||
templateUrl: "datagrid-action-overflow.html"
|
||||
})
|
||||
|
||||
export class DatagridActionOverflow {
|
||||
}
|
4
harbor-app/src/app/shared/filter/filter.component.css
Normal file
4
harbor-app/src/app/shared/filter/filter.component.css
Normal file
@ -0,0 +1,4 @@
|
||||
.filter-icon {
|
||||
position: relative;
|
||||
right: -12px;
|
||||
}
|
4
harbor-app/src/app/shared/filter/filter.component.html
Normal file
4
harbor-app/src/app/shared/filter/filter.component.html
Normal file
@ -0,0 +1,4 @@
|
||||
<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>
|
43
harbor-app/src/app/shared/filter/filter.component.ts
Normal file
43
harbor-app/src/app/shared/filter/filter.component.ts
Normal file
@ -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<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());
|
||||
}
|
||||
}
|
@ -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]
|
||||
})
|
||||
|
72
harbor-app/src/app/user/new-user-form.component.html
Normal file
72
harbor-app/src/app/user/new-user-form.component.html
Normal file
@ -0,0 +1,72 @@
|
||||
<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>
|
52
harbor-app/src/app/user/new-user-form.component.ts
Normal file
52
harbor-app/src/app/user/new-user-form.component.ts
Normal file
@ -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<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();
|
||||
}
|
||||
}
|
||||
}
|
19
harbor-app/src/app/user/new-user-modal.component.html
Normal file
19
harbor-app/src/app/user/new-user-modal.component.html
Normal file
@ -0,0 +1,19 @@
|
||||
<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>
|
106
harbor-app/src/app/user/new-user-modal.component.ts
Normal file
106
harbor-app/src/app/user/new-user-modal.component.ts
Normal file
@ -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<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;
|
||||
});
|
||||
}
|
||||
}
|
32
harbor-app/src/app/user/user.component.css
Normal file
32
harbor-app/src/app/user/user.component.css
Normal file
@ -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;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<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()">
|
||||
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Administrator</clr-dg-column>
|
||||
<clr-dg-column>Email</clr-dg-column>
|
||||
<clr-dg-column>Registration time</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let user of users" [clrDgItem]="user">
|
||||
<clr-dg-cell>{{user.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.has_admin_role?"Yes":"No"}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{user.email}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
{{user.creation_time}}
|
||||
<clr-dg-action-overflow>
|
||||
<a href="javascript:void(0)" class="dropdown-item" (click)="changeAdminRole(user)">{{user.has_admin_role?"Disable administrator":"Enable administrator"}}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteUser(user.user_id)">Delete</a>
|
||||
</clr-dg-action-overflow>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{users.length}} users</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
<new-user-modal (addNew)="addUserToList($event)"></new-user-modal>
|
||||
</div>
|
@ -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 {}
|
||||
|
||||
export class UserComponent implements OnInit {
|
||||
users: User[] = [];
|
||||
originalUsers: Promise<User[]>;
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
59
harbor-app/src/app/user/user.service.ts
Normal file
59
harbor-app/src/app/user/user.service.ts
Normal file
@ -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<any> {
|
||||
return Promise.reject(error.message || error);
|
||||
}
|
||||
|
||||
//Get the user list
|
||||
getUsers(): Promise<User[]> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
return this.http.put(userMgmtEndpoint+"/"+user.user_id, JSON.stringify(user), this.httpOptions)
|
||||
.toPromise()
|
||||
.then(() => null)
|
||||
.catch(error => this.handleError(error));
|
||||
}
|
||||
}
|
15
harbor-app/src/app/user/user.ts
Normal file
15
harbor-app/src/app/user/user.ts
Normal file
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user