Merge pull request #1816 from steven-zou/dev

Add general message handler to handle the error messages as described in #1694
This commit is contained in:
kun wang 2017-03-27 18:57:43 +08:00 committed by GitHub
commit b433b2d54f
16 changed files with 228 additions and 52 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 { 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

View File

@ -8,9 +8,11 @@ export class MessageService {
private messageAnnouncedSource = new Subject<Message>();
private appLevelAnnouncedSource = new Subject<Message>();
private clearSource = new Subject<boolean>();
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);
}
}

View File

@ -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,

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 {
font-weight: bolder;
font-size: 4em;
color: #A32100;
vertical-align: middle;
}

View File

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

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 { 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 {

View File

@ -36,6 +36,9 @@
</div>
</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>

View File

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

View File

@ -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<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 { 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);

View File

@ -17,7 +17,7 @@
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_REG_NAME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let user of users" [clrDgItem]="user">
<clr-dg-action-overflow>
<clr-dg-action-overflow [hidden]="isMySelf(user.user_id)">
<button class="action-item" (click)="changeAdminRole(user)">{{adminActions(user)}}</button>
<button class="action-item" (click)="deleteUser(user)">{{'USER.DEL_ACTION' | translate}}</button>
</clr-dg-action-overflow>

View File

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

View File

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

View File

@ -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": "服务器出现内部错误,请求无法完成"
}