diff --git a/src/ui_ng/src/app/global-message/message.component.ts b/src/ui_ng/src/app/global-message/message.component.ts index 80e46efae..d543fedf7 100644 --- a/src/ui_ng/src/app/global-message/message.component.ts +++ b/src/ui_ng/src/app/global-message/message.component.ts @@ -1,6 +1,6 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy } from '@angular/core'; import { Router } from '@angular/router'; - +import { Subscription } from 'rxjs/Subscription'; import { TranslateService } from '@ngx-translate/core'; import { Message } from './message'; @@ -12,7 +12,7 @@ import { AlertType, dismissInterval, httpStatusCode, CommonRoutes } from '../sha selector: 'global-message', templateUrl: 'message.component.html' }) -export class MessageComponent implements OnInit { +export class MessageComponent implements OnInit, OnDestroy { @Input() isAppLevel: boolean; globalMessage: Message = new Message(); @@ -20,6 +20,10 @@ export class MessageComponent implements OnInit { messageText: string = ""; private timer: any = null; + private appLevelMsgSub: Subscription; + private msgSub: Subscription; + private clearSub: Subscription; + constructor( private messageService: MessageService, private router: Router, @@ -28,7 +32,7 @@ export class MessageComponent implements OnInit { ngOnInit(): void { //Only subscribe application level message if (this.isAppLevel) { - this.messageService.appLevelAnnounced$.subscribe( + this.appLevelMsgSub = this.messageService.appLevelAnnounced$.subscribe( message => { this.globalMessageOpened = true; this.globalMessage = message; @@ -39,7 +43,7 @@ export class MessageComponent implements OnInit { ) } else { //Only subscribe general messages - this.messageService.messageAnnounced$.subscribe( + this.msgSub = this.messageService.messageAnnounced$.subscribe( message => { this.globalMessageOpened = true; this.globalMessage = message; @@ -53,6 +57,24 @@ export class MessageComponent implements OnInit { } ); } + + this.clearSub = this.messageService.clearChan$.subscribe(clear => { + this.onClose(); + }); + } + + ngOnDestroy() { + if (this.appLevelMsgSub) { + this.appLevelMsgSub.unsubscribe(); + } + + if (this.msgSub) { + this.msgSub.unsubscribe(); + } + + if (this.clearSub) { + this.clearSub.unsubscribe(); + } } //Translate or refactor the message shown to user @@ -65,20 +87,12 @@ export class MessageComponent implements OnInit { } } - //Override key for HTTP 401 and 403 - if (this.globalMessage.statusCode === httpStatusCode.Unauthorized) { - key = "UNAUTHORIZED_ERROR"; - } else if (this.globalMessage.statusCode === httpStatusCode.Forbidden) { - key = "FORBIDDEN_ERROR"; - } - this.translate.get(key, { 'param': param }).subscribe((res: string) => this.messageText = res); } public get needAuth(): boolean { return this.globalMessage ? - (this.globalMessage.statusCode === httpStatusCode.Unauthorized) || - (this.globalMessage.statusCode === httpStatusCode.Forbidden) : false; + this.globalMessage.statusCode === httpStatusCode.Unauthorized : false; } //Show message text diff --git a/src/ui_ng/src/app/global-message/message.service.ts b/src/ui_ng/src/app/global-message/message.service.ts index edd10fb27..9deaceaa1 100644 --- a/src/ui_ng/src/app/global-message/message.service.ts +++ b/src/ui_ng/src/app/global-message/message.service.ts @@ -8,9 +8,11 @@ export class MessageService { private messageAnnouncedSource = new Subject(); private appLevelAnnouncedSource = new Subject(); + private clearSource = new Subject(); messageAnnounced$ = this.messageAnnouncedSource.asObservable(); appLevelAnnounced$ = this.appLevelAnnouncedSource.asObservable(); + clearChan$ = this.clearSource.asObservable(); announceMessage(statusCode: number, message: string, alertType: AlertType) { this.messageAnnouncedSource.next(Message.newMessage(statusCode, message, alertType)); @@ -19,4 +21,8 @@ export class MessageService { announceAppLevelMessage(statusCode: number, message: string, alertType: AlertType) { this.appLevelAnnouncedSource.next(Message.newMessage(statusCode, message, alertType)); } + + clear() { + this.clearSource.next(true); + } } \ No newline at end of file diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts index 583071bfb..f95e80698 100644 --- a/src/ui_ng/src/app/harbor-routing.module.ts +++ b/src/ui_ng/src/app/harbor-routing.module.ts @@ -37,7 +37,7 @@ import { MemberGuard } from './shared/route/member-guard-activate.service'; const harborRoutes: Routes = [ { path: '', redirectTo: 'harbor', pathMatch: 'full' }, - { path: 'password-reset', component: ResetPasswordComponent }, + { path: 'reset_password', component: ResetPasswordComponent }, { path: 'harbor', component: HarborShellComponent, diff --git a/src/ui_ng/src/app/shared/message-handler/message-handler.service.ts b/src/ui_ng/src/app/shared/message-handler/message-handler.service.ts new file mode 100644 index 000000000..79bf7f0e2 --- /dev/null +++ b/src/ui_ng/src/app/shared/message-handler/message-handler.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core' +import { Subject } from 'rxjs/Subject'; + +import { MessageService } from '../../global-message/message.service'; +import { AlertType, httpStatusCode } from '../../shared/shared.const'; + +@Injectable() +export class MessageHandlerService { + + constructor(private msgService: MessageService) { } + + //Handle the error and map it to the suitable message + //base on the status code of error. + + public handleError(error: any | string): void { + if (!error) { + return; + } + + if (!error.statusCode) { + //treat as string message + let msg = "" + error; + this.msgService.announceMessage(500, msg, AlertType.DANGER); + } else { + let msg = 'UNKNOWN_ERROR'; + switch (error.statusCode) { + case 400: + msg = "BAD_REQUEST_ERROR"; + break; + case 401: + msg = "UNAUTHORIZED_ERROR"; + this.msgService.announceAppLevelMessage(error.statusCode, msg, AlertType.DANGER); + return; + case 403: + msg = "FORBIDDEN_ERROR"; + break; + case 404: + msg = "NOT_FOUND_ERROR"; + break; + case 409: + msg = "CONFLICT_ERROR"; + break; + case 500: + msg = "SERVER_ERROR"; + break; + default: + break; + } + this.msgService.announceMessage(error.statusCode, msg, AlertType.DANGER); + } + } + + public showSuccess(message: string): void { + if (message && message.trim() != "") { + this.msgService.announceMessage(200, message, AlertType.SUCCESS); + } + } + + public showInfo(message: string): void { + if (message && message.trim() != "") { + this.msgService.announceMessage(200, message, AlertType.INFO); + } + } + + public showWarning(message: string): void { + if (message && message.trim() != "") { + this.msgService.announceMessage(400, message, AlertType.WARNING); + } + } + + public clear(): void { + this.msgService.clear(); + } + + public isAppLevel(error): boolean { + return error && error.statusCode === httpStatusCode.Unauthorized; + } +} \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/not-found/not-found.component.css b/src/ui_ng/src/app/shared/not-found/not-found.component.css index 9bbdb1397..2c908a39c 100644 --- a/src/ui_ng/src/app/shared/not-found/not-found.component.css +++ b/src/ui_ng/src/app/shared/not-found/not-found.component.css @@ -11,7 +11,6 @@ .status-code { font-weight: bolder; font-size: 4em; - color: #A32100; vertical-align: middle; } diff --git a/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts b/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts index 0315973a6..b40ddb853 100644 --- a/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts +++ b/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts @@ -10,13 +10,15 @@ import { SessionService } from '../../shared/session.service'; import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const'; import { AppConfigService } from '../../app-config.service'; import { maintainUrlQueryParmas } from '../../shared/shared.utils'; +import { MessageHandlerService } from '../message-handler/message-handler.service'; @Injectable() export class AuthCheckGuard implements CanActivate, CanActivateChild { constructor( private authService: SessionService, private router: Router, - private appConfigService: AppConfigService) { } + private appConfigService: AppConfigService, + private msgHandler: MessageHandlerService) { } private isGuest(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { const proRegExp = /\/harbor\/projects\/[\d]+\/.+/i; @@ -29,6 +31,9 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { + //When routing change, clear + this.msgHandler.clear(); + return new Promise((resolve, reject) => { //Before activating, we firstly need to confirm whether the route is coming from peer part - admiral let queryParams = route.queryParams; diff --git a/src/ui_ng/src/app/shared/shared.module.ts b/src/ui_ng/src/app/shared/shared.module.ts index 958b63911..88829d067 100644 --- a/src/ui_ng/src/app/shared/shared.module.ts +++ b/src/ui_ng/src/app/shared/shared.module.ts @@ -37,6 +37,8 @@ import { MemberGuard } from './route/member-guard-activate.service'; import { ListProjectROComponent } from './list-project-ro/list-project-ro.component'; import { ListRepositoryROComponent } from './list-repository-ro/list-repository-ro.component'; +import { MessageHandlerService } from './message-handler/message-handler.service'; + @NgModule({ imports: [ CoreModule, @@ -88,7 +90,8 @@ import { ListRepositoryROComponent } from './list-repository-ro/list-repository- AuthCheckGuard, SignInGuard, LeavingConfigRouteDeactivate, - MemberGuard + MemberGuard, + MessageHandlerService ] }) export class SharedModule { diff --git a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html index df9828631..943cf3db1 100644 --- a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html +++ b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html @@ -36,6 +36,9 @@
-
Storage
+
+
{{freeStorage}}GB | {{totalStorage}}GB
+
[STORAGE]
+
\ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts index 2f6e8032b..d3451d49d 100644 --- a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts +++ b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts @@ -9,6 +9,7 @@ import { MessageService } from '../../global-message/message.service'; import { Statistics } from './statistics'; import { SessionService } from '../session.service'; +import { Volumes } from './volumes'; @Component({ selector: 'statistics-panel', @@ -20,6 +21,7 @@ import { SessionService } from '../session.service'; export class StatisticsPanelComponent implements OnInit { private originalCopy: Statistics = new Statistics(); + private volumesInfo: Volumes = new Volumes(); constructor( private statistics: StatisticsService, @@ -27,23 +29,46 @@ export class StatisticsPanelComponent implements OnInit { private session: SessionService) { } ngOnInit(): void { - if (this.session.getCurrentUser()) { + if (this.isValidSession) { this.getStatistics(); + this.getVolumes(); } } - getStatistics(): void { + public get totalStorage(): number { + return this.getGBFromBytes(this.volumesInfo.storage.total); + } + + public get freeStorage(): number { + return this.getGBFromBytes(this.volumesInfo.storage.free); + } + + public getStatistics(): void { this.statistics.getStatistics() .then(statistics => this.originalCopy = statistics) .catch(error => { if (!accessErrorHandler(error, this.msgService)) { this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING); } - }) + }); + } + + public getVolumes(): void { + this.statistics.getVolumes() + .then(volumes => this.volumesInfo = volumes) + .catch(error => { + if (!accessErrorHandler(error, this.msgService)) { + this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING); + } + }); } public get isValidSession(): boolean { let user = this.session.getCurrentUser(); return user && user.has_admin_role > 0; } + + private getGBFromBytes(bytes: number): number { + return Math.round((bytes / (1024 * 1024 * 1024))); + } } \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics.service.ts b/src/ui_ng/src/app/shared/statictics/statistics.service.ts index da2b0bafa..50207eaab 100644 --- a/src/ui_ng/src/app/shared/statictics/statistics.service.ts +++ b/src/ui_ng/src/app/shared/statictics/statistics.service.ts @@ -3,8 +3,10 @@ import { Headers, Http, RequestOptions } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { Statistics } from './statistics'; +import { Volumes } from './volumes'; -export const statisticsEndpoint = "/api/statistics"; +const statisticsEndpoint = "/api/statistics"; +const volumesEndpoint = "/api/systeminfo/volumes"; /** * Declare service to handle the top repositories * @@ -28,4 +30,10 @@ export class StatisticsService { .then(response => response.json() as Statistics) .catch(error => Promise.reject(error)); } + + getVolumes(): Promise { + return this.http.get(volumesEndpoint, this.options).toPromise() + .then(response => response.json() as Volumes) + .catch(error => Promise.reject(error)); + } } \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/volumes.ts b/src/ui_ng/src/app/shared/statictics/volumes.ts new file mode 100644 index 000000000..b44fb0324 --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/volumes.ts @@ -0,0 +1,17 @@ +export class Volumes { + constructor(){ + this.storage = new Storage(); + } + + storage: Storage; +} + +export class Storage { + constructor(){ + this.total = 0; + this.free = 0; + } + + total: number; + free: number; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/user/new-user-modal.component.ts b/src/ui_ng/src/app/user/new-user-modal.component.ts index 5c55e0d6b..026006f1d 100644 --- a/src/ui_ng/src/app/user/new-user-modal.component.ts +++ b/src/ui_ng/src/app/user/new-user-modal.component.ts @@ -6,10 +6,8 @@ import { User } from './user'; import { SessionService } from '../shared/session.service'; import { UserService } from './user.service'; -import { errorHandler, accessErrorHandler } from '../shared/shared.utils'; -import { MessageService } from '../global-message/message.service'; -import { AlertType, httpStatusCode } from '../shared/shared.const'; import { InlineAlertComponent } from '../shared/inline-alert/inline-alert.component'; +import { MessageHandlerService } from '../shared/message-handler/message-handler.service'; @Component({ selector: "new-user-modal", @@ -26,7 +24,7 @@ export class NewUserModalComponent { constructor(private session: SessionService, private userService: UserService, - private msgService: MessageService) { } + private msgHandler: MessageHandlerService) { } @ViewChild(NewUserFormComponent) private newUserForm: NewUserFormComponent; @@ -45,10 +43,6 @@ export class NewUserModalComponent { return this.newUserForm.isValid && this.error == null; } - public get errorMessage(): string { - return errorHandler(this.error); - } - formValueChange(flag: boolean): void { if (this.error != null) { this.error = null;//clear error @@ -114,12 +108,13 @@ export class NewUserModalComponent { this.addNew.emit(u); this.opened = false; - this.msgService.announceMessage(200, "USER.SAVE_SUCCESS", AlertType.SUCCESS); + this.msgHandler.showSuccess("USER.SAVE_SUCCESS"); }) .catch(error => { this.onGoing = false; this.error = error; - if(accessErrorHandler(error, this.msgService)){ + if(this.msgHandler.isAppLevel(error)){ + this.msgHandler.handleError(error); this.opened = false; }else{ this.inlineAlert.showInlineError(error); diff --git a/src/ui_ng/src/app/user/user.component.html b/src/ui_ng/src/app/user/user.component.html index 45cb9f4ee..48c1987d1 100644 --- a/src/ui_ng/src/app/user/user.component.html +++ b/src/ui_ng/src/app/user/user.component.html @@ -17,7 +17,7 @@ {{'USER.COLUMN_EMAIL' | translate}} {{'USER.COLUMN_REG_NAME' | translate}} - + diff --git a/src/ui_ng/src/app/user/user.component.ts b/src/ui_ng/src/app/user/user.component.ts index 53b3357a8..471307608 100644 --- a/src/ui_ng/src/app/user/user.component.ts +++ b/src/ui_ng/src/app/user/user.component.ts @@ -8,9 +8,10 @@ import { NewUserModalComponent } from './new-user-modal.component'; import { TranslateService } from '@ngx-translate/core'; import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service'; import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message'; -import { ConfirmationState, ConfirmationTargets, AlertType, httpStatusCode } from '../shared/shared.const' -import { errorHandler, accessErrorHandler } from '../shared/shared.utils'; -import { MessageService } from '../global-message/message.service'; +import { ConfirmationState, ConfirmationTargets } from '../shared/shared.const' +import { MessageHandlerService } from '../shared/message-handler/message-handler.service'; + +import { SessionService } from '../shared/session.service'; @Component({ selector: 'harbor-user', @@ -35,7 +36,8 @@ export class UserComponent implements OnInit, OnDestroy { private userService: UserService, private translate: TranslateService, private deletionDialogService: ConfirmationDialogService, - private msgService: MessageService) { + private msgHandler: MessageHandlerService, + private session: SessionService) { this.deletionSubscription = deletionDialogService.confirmationConfirm$.subscribe(confirmed => { if (confirmed && confirmed.source === ConfirmationTargets.USER && @@ -45,6 +47,17 @@ export class UserComponent implements OnInit, OnDestroy { }); } + private isMySelf(uid: number): boolean { + let currentUser = this.session.getCurrentUser(); + if (currentUser) { + if (currentUser.user_id === uid) { + return true; + } + } + + return false; + } + private isMatchFilterTerm(terms: string, testedItem: string): boolean { return testedItem.indexOf(terms) != -1; } @@ -101,6 +114,10 @@ export class UserComponent implements OnInit, OnDestroy { return; } + if (this.isMySelf(user.user_id)) { + return; + } + //Value copy let updatedUser: User = { user_id: user.user_id @@ -118,9 +135,7 @@ export class UserComponent implements OnInit, OnDestroy { user.has_admin_role = updatedUser.has_admin_role; }) .catch(error => { - if (!accessErrorHandler(error, this.msgService)) { - this.msgService.announceMessage(500, errorHandler(error), AlertType.DANGER); - } + this.msgHandler.handleError(error); }) } @@ -130,6 +145,10 @@ export class UserComponent implements OnInit, OnDestroy { return; } + if (this.isMySelf(user.user_id)) { + return; //Double confirm + } + //Confirm deletion let msg: ConfirmationMessage = new ConfirmationMessage( "USER.DELETION_TITLE", @@ -148,13 +167,11 @@ export class UserComponent implements OnInit, OnDestroy { //and then view refreshed this.originalUsers.then(users => { this.users = users.filter(u => u.user_id != user.user_id); - this.msgService.announceMessage(500, "USER.DELETE_SUCCESS", AlertType.SUCCESS); + this.msgHandler.showSuccess("USER.DELETE_SUCCESS"); }); }) .catch(error => { - if (!accessErrorHandler(error, this.msgService)) { - this.msgService.announceMessage(500, errorHandler(error), AlertType.DANGER); - } + this.msgHandler.handleError(error); }); } @@ -172,9 +189,7 @@ export class UserComponent implements OnInit, OnDestroy { }) .catch(error => { this.onGoing = false; - if (!accessErrorHandler(error, this.msgService)) { - this.msgService.announceMessage(500, errorHandler(error), AlertType.DANGER); - } + this.msgHandler.handleError(error); }); } diff --git a/src/ui_ng/src/i18n/lang/en-lang.json b/src/ui_ng/src/i18n/lang/en-lang.json index 426d0febb..9bbabfb7a 100644 --- a/src/ui_ng/src/i18n/lang/en-lang.json +++ b/src/ui_ng/src/i18n/lang/en-lang.json @@ -413,7 +413,11 @@ "BACK": "Back" }, "UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later", - "UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue the operation", - "FORBIDDEN_ERROR": "You are not allowed to perform this operation", - "GENERAL_ERROR": "Errors have occurred when performing service call: {{param}}" + "UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action", + "FORBIDDEN_ERROR": "You do not have the proper privileges to perform the action", + "GENERAL_ERROR": "Errors have occurred when performing service call: {{param}}", + "BAD_REQUEST_ERROR": "We are unable to perform your action because of a bad request", + "NOT_FOUND_ERROR": "Your request can not be completed because the object does not exist", + "CONFLICT_ERROR": "We are unable to perform your action because your submission has conflicts", + "SERVER_ERROR": "We are unable to perform your action because internal server errors have occurred" } \ No newline at end of file diff --git a/src/ui_ng/src/i18n/lang/zh-lang.json b/src/ui_ng/src/i18n/lang/zh-lang.json index ef3370752..e86c0c214 100644 --- a/src/ui_ng/src/i18n/lang/zh-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-lang.json @@ -415,5 +415,9 @@ "UNKNOWN_ERROR": "发生未知错误,请稍后再试", "UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续", "FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限", - "GENERAL_ERROR": "调用后台服务时出现错误: {{param}}" + "GENERAL_ERROR": "调用后台服务时出现错误: {{param}}", + "BAD_REQUEST_ERROR": "错误请求导致无法完成操作", + "NOT_FOUND_ERROR": "对象不存在故无法完成你的请求", + "CONFLICT_ERROR": "你的提交包含冲突故操作无法完成", + "SERVER_ERROR": "服务器出现内部错误,请求无法完成" } \ No newline at end of file