fix issue #1694: add general message handler to handler the error and error messages

This commit is contained in:
Steven Zou 2017-03-27 18:39:53 +08:00
parent 3a3b1c5ad5
commit 3c05a35303
15 changed files with 209 additions and 55 deletions

View File

@ -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 { Router } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Message } from './message'; import { Message } from './message';
@ -12,7 +12,7 @@ import { AlertType, dismissInterval, httpStatusCode, CommonRoutes } from '../sha
selector: 'global-message', selector: 'global-message',
templateUrl: 'message.component.html' templateUrl: 'message.component.html'
}) })
export class MessageComponent implements OnInit { export class MessageComponent implements OnInit, OnDestroy {
@Input() isAppLevel: boolean; @Input() isAppLevel: boolean;
globalMessage: Message = new Message(); globalMessage: Message = new Message();
@ -20,6 +20,10 @@ export class MessageComponent implements OnInit {
messageText: string = ""; messageText: string = "";
private timer: any = null; private timer: any = null;
private appLevelMsgSub: Subscription;
private msgSub: Subscription;
private clearSub: Subscription;
constructor( constructor(
private messageService: MessageService, private messageService: MessageService,
private router: Router, private router: Router,
@ -28,7 +32,7 @@ export class MessageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
//Only subscribe application level message //Only subscribe application level message
if (this.isAppLevel) { if (this.isAppLevel) {
this.messageService.appLevelAnnounced$.subscribe( this.appLevelMsgSub = this.messageService.appLevelAnnounced$.subscribe(
message => { message => {
this.globalMessageOpened = true; this.globalMessageOpened = true;
this.globalMessage = message; this.globalMessage = message;
@ -39,7 +43,7 @@ export class MessageComponent implements OnInit {
) )
} else { } else {
//Only subscribe general messages //Only subscribe general messages
this.messageService.messageAnnounced$.subscribe( this.msgSub = this.messageService.messageAnnounced$.subscribe(
message => { message => {
this.globalMessageOpened = true; this.globalMessageOpened = true;
this.globalMessage = message; 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 //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); this.translate.get(key, { 'param': param }).subscribe((res: string) => this.messageText = res);
} }
public get needAuth(): boolean { public get needAuth(): boolean {
return this.globalMessage ? return this.globalMessage ?
(this.globalMessage.statusCode === httpStatusCode.Unauthorized) || this.globalMessage.statusCode === httpStatusCode.Unauthorized : false;
(this.globalMessage.statusCode === httpStatusCode.Forbidden) : false;
} }
//Show message text //Show message text

View File

@ -8,9 +8,11 @@ export class MessageService {
private messageAnnouncedSource = new Subject<Message>(); private messageAnnouncedSource = new Subject<Message>();
private appLevelAnnouncedSource = new Subject<Message>(); private appLevelAnnouncedSource = new Subject<Message>();
private clearSource = new Subject<boolean>();
messageAnnounced$ = this.messageAnnouncedSource.asObservable(); messageAnnounced$ = this.messageAnnouncedSource.asObservable();
appLevelAnnounced$ = this.appLevelAnnouncedSource.asObservable(); appLevelAnnounced$ = this.appLevelAnnouncedSource.asObservable();
clearChan$ = this.clearSource.asObservable();
announceMessage(statusCode: number, message: string, alertType: AlertType) { announceMessage(statusCode: number, message: string, alertType: AlertType) {
this.messageAnnouncedSource.next(Message.newMessage(statusCode, message, alertType)); this.messageAnnouncedSource.next(Message.newMessage(statusCode, message, alertType));
@ -19,4 +21,8 @@ export class MessageService {
announceAppLevelMessage(statusCode: number, message: string, alertType: AlertType) { announceAppLevelMessage(statusCode: number, message: string, alertType: AlertType) {
this.appLevelAnnouncedSource.next(Message.newMessage(statusCode, message, alertType)); this.appLevelAnnouncedSource.next(Message.newMessage(statusCode, message, alertType));
} }
clear() {
this.clearSource.next(true);
}
} }

View File

@ -37,7 +37,7 @@ import { MemberGuard } from './shared/route/member-guard-activate.service';
const harborRoutes: Routes = [ const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' }, { path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'password-reset', component: ResetPasswordComponent }, { path: 'reset_password', component: ResetPasswordComponent },
{ {
path: 'harbor', path: 'harbor',
component: HarborShellComponent, component: HarborShellComponent,

View File

@ -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;
}
}

View File

@ -11,7 +11,6 @@
.status-code { .status-code {
font-weight: bolder; font-weight: bolder;
font-size: 4em; font-size: 4em;
color: #A32100;
vertical-align: middle; vertical-align: middle;
} }

View File

@ -10,13 +10,15 @@ import { SessionService } from '../../shared/session.service';
import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const'; import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
import { maintainUrlQueryParmas } from '../../shared/shared.utils'; import { maintainUrlQueryParmas } from '../../shared/shared.utils';
import { MessageHandlerService } from '../message-handler/message-handler.service';
@Injectable() @Injectable()
export class AuthCheckGuard implements CanActivate, CanActivateChild { export class AuthCheckGuard implements CanActivate, CanActivateChild {
constructor( constructor(
private authService: SessionService, private authService: SessionService,
private router: Router, private router: Router,
private appConfigService: AppConfigService) { } private appConfigService: AppConfigService,
private msgHandler: MessageHandlerService) { }
private isGuest(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { private isGuest(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const proRegExp = /\/harbor\/projects\/[\d]+\/.+/i; const proRegExp = /\/harbor\/projects\/[\d]+\/.+/i;
@ -29,6 +31,9 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
} }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
//When routing change, clear
this.msgHandler.clear();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//Before activating, we firstly need to confirm whether the route is coming from peer part - admiral //Before activating, we firstly need to confirm whether the route is coming from peer part - admiral
let queryParams = route.queryParams; let queryParams = route.queryParams;

View File

@ -37,6 +37,8 @@ import { MemberGuard } from './route/member-guard-activate.service';
import { ListProjectROComponent } from './list-project-ro/list-project-ro.component'; import { ListProjectROComponent } from './list-project-ro/list-project-ro.component';
import { ListRepositoryROComponent } from './list-repository-ro/list-repository-ro.component'; import { ListRepositoryROComponent } from './list-repository-ro/list-repository-ro.component';
import { MessageHandlerService } from './message-handler/message-handler.service';
@NgModule({ @NgModule({
imports: [ imports: [
CoreModule, CoreModule,
@ -88,7 +90,8 @@ import { ListRepositoryROComponent } from './list-repository-ro/list-repository-
AuthCheckGuard, AuthCheckGuard,
SignInGuard, SignInGuard,
LeavingConfigRouteDeactivate, LeavingConfigRouteDeactivate,
MemberGuard MemberGuard,
MessageHandlerService
] ]
}) })
export class SharedModule { export class SharedModule {

View File

@ -36,6 +36,9 @@
</div> </div>
</div> </div>
<div class="statistic-item-divider"></div> <div class="statistic-item-divider"></div>
<div class="statistic-block">Storage</div> <div class="statistic-block">
<div>{{freeStorage}}GB | {{totalStorage}}GB</div>
<div>[STORAGE]</div>
</div>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@ import { MessageService } from '../../global-message/message.service';
import { Statistics } from './statistics'; import { Statistics } from './statistics';
import { SessionService } from '../session.service'; import { SessionService } from '../session.service';
import { Volumes } from './volumes';
@Component({ @Component({
selector: 'statistics-panel', selector: 'statistics-panel',
@ -20,6 +21,7 @@ import { SessionService } from '../session.service';
export class StatisticsPanelComponent implements OnInit { export class StatisticsPanelComponent implements OnInit {
private originalCopy: Statistics = new Statistics(); private originalCopy: Statistics = new Statistics();
private volumesInfo: Volumes = new Volumes();
constructor( constructor(
private statistics: StatisticsService, private statistics: StatisticsService,
@ -27,23 +29,46 @@ export class StatisticsPanelComponent implements OnInit {
private session: SessionService) { } private session: SessionService) { }
ngOnInit(): void { ngOnInit(): void {
if (this.session.getCurrentUser()) { if (this.isValidSession) {
this.getStatistics(); 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() this.statistics.getStatistics()
.then(statistics => this.originalCopy = statistics) .then(statistics => this.originalCopy = statistics)
.catch(error => { .catch(error => {
if (!accessErrorHandler(error, this.msgService)) { if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING); 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 { public get isValidSession(): boolean {
let user = this.session.getCurrentUser(); let user = this.session.getCurrentUser();
return user && user.has_admin_role > 0; return user && user.has_admin_role > 0;
} }
private getGBFromBytes(bytes: number): number {
return Math.round((bytes / (1024 * 1024 * 1024)));
}
} }

View File

@ -3,8 +3,10 @@ import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise'; import 'rxjs/add/operator/toPromise';
import { Statistics } from './statistics'; 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 * Declare service to handle the top repositories
* *
@ -28,4 +30,10 @@ export class StatisticsService {
.then(response => response.json() as Statistics) .then(response => response.json() as Statistics)
.catch(error => Promise.reject(error)); .catch(error => Promise.reject(error));
} }
getVolumes(): Promise<Volumes> {
return this.http.get(volumesEndpoint, this.options).toPromise()
.then(response => response.json() as Volumes)
.catch(error => Promise.reject(error));
}
} }

View File

@ -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;
}

View File

@ -6,10 +6,8 @@ import { User } from './user';
import { SessionService } from '../shared/session.service'; import { SessionService } from '../shared/session.service';
import { UserService } from './user.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 { InlineAlertComponent } from '../shared/inline-alert/inline-alert.component';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
@Component({ @Component({
selector: "new-user-modal", selector: "new-user-modal",
@ -26,7 +24,7 @@ export class NewUserModalComponent {
constructor(private session: SessionService, constructor(private session: SessionService,
private userService: UserService, private userService: UserService,
private msgService: MessageService) { } private msgHandler: MessageHandlerService) { }
@ViewChild(NewUserFormComponent) @ViewChild(NewUserFormComponent)
private newUserForm: NewUserFormComponent; private newUserForm: NewUserFormComponent;
@ -45,10 +43,6 @@ export class NewUserModalComponent {
return this.newUserForm.isValid && this.error == null; return this.newUserForm.isValid && this.error == null;
} }
public get errorMessage(): string {
return errorHandler(this.error);
}
formValueChange(flag: boolean): void { formValueChange(flag: boolean): void {
if (this.error != null) { if (this.error != null) {
this.error = null;//clear error this.error = null;//clear error
@ -114,12 +108,13 @@ export class NewUserModalComponent {
this.addNew.emit(u); this.addNew.emit(u);
this.opened = false; this.opened = false;
this.msgService.announceMessage(200, "USER.SAVE_SUCCESS", AlertType.SUCCESS); this.msgHandler.showSuccess("USER.SAVE_SUCCESS");
}) })
.catch(error => { .catch(error => {
this.onGoing = false; this.onGoing = false;
this.error = error; this.error = error;
if(accessErrorHandler(error, this.msgService)){ if(this.msgHandler.isAppLevel(error)){
this.msgHandler.handleError(error);
this.opened = false; this.opened = false;
}else{ }else{
this.inlineAlert.showInlineError(error); this.inlineAlert.showInlineError(error);

View File

@ -8,9 +8,8 @@ import { NewUserModalComponent } from './new-user-modal.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service'; import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message'; import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationState, ConfirmationTargets, AlertType, httpStatusCode } from '../shared/shared.const' import { ConfirmationState, ConfirmationTargets } from '../shared/shared.const'
import { errorHandler, accessErrorHandler } from '../shared/shared.utils'; import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { MessageService } from '../global-message/message.service';
import { SessionService } from '../shared/session.service'; import { SessionService } from '../shared/session.service';
@ -37,7 +36,7 @@ export class UserComponent implements OnInit, OnDestroy {
private userService: UserService, private userService: UserService,
private translate: TranslateService, private translate: TranslateService,
private deletionDialogService: ConfirmationDialogService, private deletionDialogService: ConfirmationDialogService,
private msgService: MessageService, private msgHandler: MessageHandlerService,
private session: SessionService) { private session: SessionService) {
this.deletionSubscription = deletionDialogService.confirmationConfirm$.subscribe(confirmed => { this.deletionSubscription = deletionDialogService.confirmationConfirm$.subscribe(confirmed => {
if (confirmed && if (confirmed &&
@ -50,8 +49,8 @@ export class UserComponent implements OnInit, OnDestroy {
private isMySelf(uid: number): boolean { private isMySelf(uid: number): boolean {
let currentUser = this.session.getCurrentUser(); let currentUser = this.session.getCurrentUser();
if(currentUser){ if (currentUser) {
if(currentUser.user_id === uid ){ if (currentUser.user_id === uid) {
return true; return true;
} }
} }
@ -115,7 +114,7 @@ export class UserComponent implements OnInit, OnDestroy {
return; return;
} }
if(this.isMySelf(user.user_id)){ if (this.isMySelf(user.user_id)) {
return; return;
} }
@ -136,9 +135,7 @@ export class UserComponent implements OnInit, OnDestroy {
user.has_admin_role = updatedUser.has_admin_role; user.has_admin_role = updatedUser.has_admin_role;
}) })
.catch(error => { .catch(error => {
if (!accessErrorHandler(error, this.msgService)) { this.msgHandler.handleError(error);
this.msgService.announceMessage(500, errorHandler(error), AlertType.DANGER);
}
}) })
} }
@ -148,7 +145,7 @@ export class UserComponent implements OnInit, OnDestroy {
return; return;
} }
if(this.isMySelf(user.user_id)){ if (this.isMySelf(user.user_id)) {
return; //Double confirm return; //Double confirm
} }
@ -170,13 +167,11 @@ export class UserComponent implements OnInit, OnDestroy {
//and then view refreshed //and then view refreshed
this.originalUsers.then(users => { this.originalUsers.then(users => {
this.users = users.filter(u => u.user_id != user.user_id); 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 => { .catch(error => {
if (!accessErrorHandler(error, this.msgService)) { this.msgHandler.handleError(error);
this.msgService.announceMessage(500, errorHandler(error), AlertType.DANGER);
}
}); });
} }
@ -194,9 +189,7 @@ export class UserComponent implements OnInit, OnDestroy {
}) })
.catch(error => { .catch(error => {
this.onGoing = false; this.onGoing = false;
if (!accessErrorHandler(error, this.msgService)) { this.msgHandler.handleError(error);
this.msgService.announceMessage(500, errorHandler(error), AlertType.DANGER);
}
}); });
} }

View File

@ -413,7 +413,11 @@
"BACK": "Back" "BACK": "Back"
}, },
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later", "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", "UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action",
"FORBIDDEN_ERROR": "You are not allowed to perform this operation", "FORBIDDEN_ERROR": "You do not have the proper privileges to perform the action",
"GENERAL_ERROR": "Errors have occurred when performing service call: {{param}}" "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"
} }

View File

@ -415,5 +415,9 @@
"UNKNOWN_ERROR": "发生未知错误,请稍后再试", "UNKNOWN_ERROR": "发生未知错误,请稍后再试",
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续", "UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限", "FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限",
"GENERAL_ERROR": "调用后台服务时出现错误: {{param}}" "GENERAL_ERROR": "调用后台服务时出现错误: {{param}}",
"BAD_REQUEST_ERROR": "错误请求导致无法完成操作",
"NOT_FOUND_ERROR": "对象不存在故无法完成你的请求",
"CONFLICT_ERROR": "你的提交包含冲突故操作无法完成",
"SERVER_ERROR": "服务器出现内部错误,请求无法完成"
} }