Unverified Commit e8663d9c authored by Nestor Diaz's avatar Nestor Diaz Committed by GitHub

Improve error visualisation (#421)

Improve error visualisation
parent a0ed7146
Pipeline #108136 passed with stages
in 15 minutes and 38 seconds
......@@ -18237,6 +18237,14 @@
}
}
},
"ngx-toastr": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-13.1.0.tgz",
"integrity": "sha512-TS4rIfg/oPmmjKadsXLSNIN/A9LktcYPZayGhqLpzcjMud7XLLubLqbrmnH34UakMrUq6QCXXYYiU0QTMW5Qhw==",
"requires": {
"tslib": "^2.0.0"
}
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
export const DEFAULT_ERROR_MESSAGE = `Something went wrong at our side. Sorry for the inconvenience,
we are working to fix it. Please try again later and if the problem persists,
drop an email to biostudies@ebi.ac.uk`;
import {
NgModule,
ErrorHandler,
APP_INITIALIZER
} from '@angular/core';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { PathLocationStrategy, LocationStrategy } from '@angular/common';
import { RecaptchaModule, RecaptchaSettings, RECAPTCHA_SETTINGS, RECAPTCHA_BASE_URL } from 'ng-recaptcha';
......@@ -18,12 +14,12 @@ import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { MarkdownModule } from 'ngx-markdown';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SortablejsModule } from 'ngx-sortablejs';
import { ToastrModule } from 'ngx-toastr';
import { AppComponent } from './app.component';
import { AppConfig } from './app.config';
import { AppRoutingModule } from './app-routing.module';
import { AuthModule } from './auth/auth.module';
import { CoreModule } from './core/core.module';
import { GlobalErrorHandler } from './global-error.handler';
import { ThemeModule } from './theme/theme.module';
import { PagesModule } from './pages/pages.module';
......@@ -45,6 +41,9 @@ export function initConfig(config: AppConfig): () => Promise<any> {
CollapseModule.forRoot(),
AlertModule.forRoot(),
SortablejsModule.forRoot({ animation: 150 }),
ToastrModule.forRoot({
positionClass: 'toast-top-left'
}),
RecaptchaModule,
BrowserAnimationsModule,
PagesModule,
......@@ -60,7 +59,6 @@ export function initConfig(config: AppConfig): () => Promise<any> {
AppConfig,
{ provide: APP_INITIALIZER, useFactory: initConfig, deps: [AppConfig], multi: true },
{ provide: LocationStrategy, useClass: PathLocationStrategy },
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
{ provide: RECAPTCHA_BASE_URL, useValue: 'https://recaptcha.net/recaptcha/api.js' },
{
provide: RECAPTCHA_SETTINGS,
......
<st-auth-container>
<div class="alert" [ngClass]="{'alert-danger': error, 'invisible': !error}">
<div class="message">
{{error ? error.message : '&nbsp;'}}
Please check the fields below and try again, if the problem persists, drop an email to <a
href="mailto:biostudies@ebi.ac.uk">biostudies@ebi.ac.uk</a>
</div>
......
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { ErrorHandler, NgModule, Optional, SkipSelf } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { LogService } from './logger/log.service';
import { RequestStatusInterceptorService, RequestStatusServiceFactory } from './interceptors/request-status-interceptor.service';
import { AuthInterceptorService } from './interceptors/auth-interceptor.service';
import { ContextPathInterceptorService } from './interceptors/context-path-interceptor.service';
import { GlobalErrorService } from './errors/global-error.service';
import { ErrorMessageService } from './errors/error-message.service';
@NgModule({
imports: [
......@@ -11,6 +13,7 @@ import { ContextPathInterceptorService } from './interceptors/context-path-inter
],
providers: [
LogService,
ErrorMessageService,
{
provide: RequestStatusInterceptorService,
useFactory: RequestStatusServiceFactory
......@@ -28,7 +31,8 @@ import { ContextPathInterceptorService } from './interceptors/context-path-inter
provide: HTTP_INTERCEPTORS,
useClass: ContextPathInterceptorService,
multi: true
}
},
{ provide: ErrorHandler, useClass: GlobalErrorService },
]
})
export class CoreModule {
......
import { Injectable } from '@angular/core';
@Injectable()
export class ErrorMessageService {
private defaultMessage: string = `Something went wrong at our side, sorry for the inconvenience.
Please try again later and if the problem persists drop an email to`;
getMessage(): string {
return this.buildErrorMessage();
}
getPlainMessage(): string {
return this.buildErrorMessage('', true);
}
getMessageWithMailBody(errorMessage): string {
return this.buildErrorMessage(errorMessage);
}
private buildErrorMessage(errorMessage?: string, plain: boolean = false): string {
if (errorMessage) {
return`${this.defaultMessage} <a href="mailto:biostudies@ebi.ac.uk?subject=Submission Tool error&body=${errorMessage}">biostudies@ebi.ac.uk</a>`;
} else if (plain) {
return`${this.defaultMessage} biostudies@ebi.ac.uk`;
} else {
return`${this.defaultMessage} <a href="mailto:biostudies@ebi.ac.uk?subject=Submission Tool error>biostudies@ebi.ac.uk</a>`;
}
}
}
import {
ErrorHandler,
Injectable, NgZone
Injectable,
Injector,
NgZone
} from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { UserSession } from 'app/auth/shared';
import { INTERNAL_SERVER_ERROR, UNAUTHORIZED } from 'http-status-codes';
import { LogService } from './core/logger/log.service';
import { DEFAULT_ERROR_MESSAGE } from './app.constants';
import { ToastrService } from 'ngx-toastr';
import { UserSession } from 'app/auth/shared';
import { ErrorMessageService } from './error-message.service';
@Injectable()
export class GlobalErrorHandler extends ErrorHandler {
export class GlobalErrorService extends ErrorHandler {
private errors: Subject<any> = new Subject<any>();
constructor(private userSession: UserSession, private zone: NgZone, private logService: LogService) {
constructor(
private userSession: UserSession,
private zone: NgZone,
private injector: Injector,
private errorMessage: ErrorMessageService
) {
super();
}
......@@ -20,11 +27,23 @@ export class GlobalErrorHandler extends ErrorHandler {
return this.errors.asObservable();
}
private get toastr(): ToastrService {
return this.injector.get(ToastrService);
}
private showErrorToast(error): void {
const message = this.errorMessage.getMessageWithMailBody(error.message);
this.toastr.error(message, '', {
closeButton: true,
disableTimeOut: true,
enableHtml: true,
tapToDismiss: false
});
}
handleError(error): void {
// Invalid authentication credentials, probably due to the current session having expired => clean up and reload.
// NOTE: the app seems to get into a limbo state whereby the digest cycle fails to detect property changes
// any more and requests are not issued. Reloading is a workaround.
// TODO: why is this happening?
if (error.status === UNAUTHORIZED) {
this.userSession.destroy();
this.zone.runOutsideAngular(() => location.reload());
......@@ -32,16 +51,11 @@ export class GlobalErrorHandler extends ErrorHandler {
if (error.status === INTERNAL_SERVER_ERROR) {
// An error occurred that may potentially be worth handling at a global level.
this.errors.next(DEFAULT_ERROR_MESSAGE);
this.showErrorToast(error);
} else {
// TODO: post error to new logging system.
// tslint:disable-next-line: no-console
console.error(error);
this.errors.next(error);
this.showErrorToast(error);
}
this.logService.error('global-error', error);
}
}
......@@ -11,7 +11,7 @@ import { FileUpload } from '../../shared/file-upload-list.service';
class="btn btn-primary"
tooltip="Download"
container="body"
placement="bottom"
placement="left"
(click)="onFileDownload($event)">
<i class="fas fa-download fa-fw"></i>
</button>
......@@ -20,7 +20,7 @@ import { FileUpload } from '../../shared/file-upload-list.service';
class="btn btn-danger"
tooltip="Delete"
container="body"
placement="bottom"
placement="left"
(click)="onFileRemove($event)">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
......@@ -28,7 +28,7 @@ import { FileUpload } from '../../shared/file-upload-list.service';
type="button" class="btn btn-warning"
tooltip="Cancel"
container="body"
placement="bottom"
placement="left"
(click)="onCancelUpload($event)">
Cancel
</button>
......
<form *ngIf="sectionForm" class="needs-validation mt-4" [formGroup]="sectionForm.form" (ngSubmit)="onSubmit($event)" novalidate>
<ng-container *ngFor="let fieldControl of sectionForm.fieldControls" [ngClass]="{'form-group row': !readonly}">
<ng-container *ngFor="let fieldControl of sectionForm.fieldControls">
<st-subm-field [fieldControl]="fieldControl" [readonly]="readonly"></st-subm-field>
</ng-container>
<ng-container *ngFor="let featureForm of sectionForm.featureForms"
[ngClass]="{'form-group row': !featureForm.isEmpty}">
<ng-container *ngFor="let featureForm of sectionForm.featureForms">
<st-subm-feature [featureForm]="featureForm" [readonly]="readonly"></st-subm-feature>
</ng-container>
</form>
......@@ -4,7 +4,7 @@ import { BsModalService } from 'ngx-bootstrap/modal';
import { Location } from '@angular/common';
import { Observable, of, Subject } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';
import { LogService } from 'app/core/logger/log.service';
import { ErrorMessageService } from 'app/core/errors/error-message.service';
import { ModalService } from 'app/shared/modal.service';
import { scrollTop } from 'app/utils';
import { SectionForm } from './shared/model/section-form.model';
......@@ -59,7 +59,7 @@ export class SubmissionEditComponent implements OnInit, OnDestroy {
private bsModalService: BsModalService,
private modalService: ModalService,
private submEditService: SubmEditService,
private logService: LogService
private errorMessage: ErrorMessageService
) {
submEditService.sectionSwitch$.pipe(
takeUntil(this.unsubscribe),
......@@ -121,14 +121,12 @@ export class SubmissionEditComponent implements OnInit, OnDestroy {
}
if (resp.error.isSome()) {
this.modalService.alert(
'Submission could not be retrieved. ' +
'Please make sure the URL is correct and contact us in case the problem persists.', 'Error', 'Ok'
).subscribe(() => {
const message = this.errorMessage.getPlainMessage();
this.modalService.alert(message, 'Error', 'Ok').subscribe(() => {
this.router.navigate(['/submissions/']);
});
this.logService.error('submission-edit', resp.error);
// tslint:disable-next-line: no-console
console.error(resp.error);
} else {
......
......@@ -8,6 +8,7 @@ import { Component } from '@angular/core';
<button *ngIf="rowData && isRowEditable" type="button" class="btn btn-primary"
(click)="onEditSubmission()"
tooltip="Edit this submission"
placement="left"
container="body">
<i class="fas fa-pencil-alt fa-fw"></i>
</button>
......@@ -15,6 +16,7 @@ import { Component } from '@angular/core';
[disabled]="isBusy"
(click)="onDeleteSubmission()"
tooltip="Delete this submission"
placement="left"
container="body">
<i *ngIf="!isBusy" class="fas fa-trash-alt fa-fw"></i>
<i *ngIf="isBusy" class="fa fa-cog fa-spin fa-fw"></i>
......
import * as HttpStatus from 'http-status-codes';
import { throwError, Observable } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { DEFAULT_ERROR_MESSAGE } from 'app/app.constants';
export class ServerError {
static defaultErrorMessage: string = `Something went wrong at our side. Sorry for the inconvenience,
we are working to fix it. Please try again later and if the problem persists,
drop an email to biostudies@ebi.ac.uk`;
data: any;
status: number = 0;
statusString: string = '';
......@@ -27,7 +30,7 @@ export class ServerError {
static fromResponse(response: HttpErrorResponse): ServerError {
const { error } = response;
const data = {
message: DEFAULT_ERROR_MESSAGE,
message: this.defaultErrorMessage,
error: {}
};
......
.alert-container {
align-items: center;
box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.18);
display: flex;
margin-left: -100%;
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
transition: margin-left .5s ease-in-out;
white-space: nowrap;
z-index: 1000;
-webkit-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.18);
-moz-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.18);
}
.close {
padding: 15px;
background: #d99997;
}
.slide-in {
margin-left: 3.5px;
}
.message {
padding: 0 20px;
}
<div role="alert" class="alert alert-toast alert-container alert-danger alert-dismissible">
<button aria-label="Close" class="close" type="button" (click)="onClose()">
<i class="fa fa-times"></i>
</button>
<div class="message" [innerHTML]="message"></div>
</div>
import { ChangeDetectorRef, Component, ElementRef, ErrorHandler } from '@angular/core';
import { GlobalErrorHandler } from 'app/global-error.handler';
@Component({
selector: 'st-error-toast',
templateUrl: './error-toast.component.html',
styleUrls: ['./error-toast.component.css']
})
export class ErrorToastComponent {
message: string = '';
/**
* Captures async errors and refreshes the UI (slides an alert in).
* @param geh - Global handler for errors.
* @param changeRef - Forces change detection on this component.
* @param rootEl - Reference to the component's wrapping element
*/
constructor(geh: ErrorHandler, changeRef: ChangeDetectorRef, private rootEl: ElementRef) {
if (geh instanceof GlobalErrorHandler) {
geh.errorDetected.subscribe(error => {
// Message conversion is bypassed to allow for plain strings as error exception objects.
if (typeof error === 'string') {
this.message = error;
} else {
this.message = this.toMessage(error);
}
changeRef.detectChanges();
rootEl.nativeElement.firstChild.classList.add('slide-in');
});
}
}
/**
* Slides the alert out of view when performing the close action.
*/
onClose(): void {
this.rootEl.nativeElement.firstChild.classList.remove('slide-in');
}
/**
* Merges different error properties to produce the string to be shown as an error message. If there is
* no network, the user is instead alerted to that.
* @param error - Error object containing fragments of the error message.
* @returns Text to be used as an error message.
*/
toMessage(error: any): string {
// There seems to be network connection
if (navigator.onLine) {
const name = error.name || '';
const message = error.message || '';
return (name + message).length === 0 ? 'Unknown error' : (name + ' ' + message);
// Definitely not connected
} else {
return 'You seem to be offline. Please check your network.';
}
}
}
export * from './header/header.component';
export * from './error-toast/error-toast.component';
export * from './sidebar/sidebar.component';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from 'app/shared/shared.module';
import { HeaderComponent, ErrorToastComponent, SidebarComponent } from './components';
import { HeaderComponent, SidebarComponent } from './components';
import { LayoutComponent, LayoutColumnComponent, LayoutHeaderComponent } from './components/layout/layout.component';
const COMPONENTS = [
HeaderComponent,
ErrorToastComponent,
SidebarComponent,
LayoutComponent,
LayoutColumnComponent,
......
......@@ -6,38 +6,25 @@
@import './theme/subtool-theme.scss';
@import './theme/subtool-ag-grid-theme.scss';
@import '~bootstrap/scss/bootstrap';
@import '~ngx-toastr/toastr-bs4-alert';
body {
padding-top: var(--header-height);
position: relative;
}
/*
NOTE: Font-size is set to 62.5% so that all the REM measurements throughout
are based on 10px sizing. So basically 1.5rem = 15px.
*/
// html {
// font-size: 62.5%;
// }
// .st * {
// box-sizing: border-box
// }
// .st {
// -moz-osx-font-smoothing: grayscale;
// -webkit-font-smoothing: antialiased;
// background: #fafafa;
// font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
// font-size: 14px;
// height: 100%;
// line-height: 1.42857143;
// margin: 0;
// padding: 0;
// }
/* Selects dropdown are append to body so the reason of having this global style here. */
/* It ensures wide content in the dropdown is shown properly. */
.ng-dropdown-panel {
width: auto !important;
}
/* Toast styles need to be global as the toast can be opened from any component */
.toast-top-left {
top: 5px;
}
.toast-container .ngx-toastr {
width: var(--toast-width);
padding: 0.2rem 1.25rem 0.2rem 50px;
}
......@@ -4,6 +4,7 @@
--sidebar-collapsed-width: 65px;
--sidebar-expanded-width: 260px;
--border-radius-small: 3px;
--toast-width: 800px;
// Colors
--blue: #3c739f;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment