Progress error status, Fixes: #146

This commit is contained in:
ziajka
2018-07-04 13:44:35 +02:00
parent ca45804b5e
commit db3e700204
9 changed files with 153 additions and 21 deletions

View File

@ -1,8 +1,20 @@
<div class="overlay" *ngIf="visible"> <div class="overlay" *ngIf="visible || error">
<div class="loading-spinner"> <div class="loading-spinner" *ngIf="visible && !error">
<mat-spinner color="primary"> <mat-spinner color="primary">
</mat-spinner> </mat-spinner>
</div> </div>
<div class="error-state" *ngIf="error">
<div class="error-icon"><mat-icon>error_outline</mat-icon></div>
<div>Error occurred: {{ error.message }}</div>
<div>
<button mat-button (click)="refresh()" matTooltip="Refresh page" >
<mat-icon>refresh</mat-icon>
</button>
<button mat-button routerLink="/" matTooltip="Go to home" >
<mat-icon>home</mat-icon>
</button>
</div>
</div>
</div> </div>

View File

@ -10,9 +10,19 @@
z-index: 1000; z-index: 1000;
} }
.loading-spinner { .loading-spinner, .error-state {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.error-state div {
text-align: center;
}
.error-icon mat-icon {
font-size: 64px;
width: 64px;
height: 64px;
}

View File

@ -1,25 +1,45 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProgressComponent } from './progress.component'; import { ProgressComponent } from './progress.component';
import { MatProgressSpinnerModule } from "@angular/material"; import { MatIconModule, MatProgressSpinnerModule } from "@angular/material";
import { ProgressService } from "./progress.service"; import { ProgressService } from "./progress.service";
import { RouterTestingModule } from "@angular/router/testing";
import { Router } from "@angular/router";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
class MockedRouter {
events: BehaviorSubject<boolean>;
constructor() {
this.events = new BehaviorSubject(true);
}
}
describe('ProgressComponent', () => { describe('ProgressComponent', () => {
let component: ProgressComponent; let component: ProgressComponent;
let fixture: ComponentFixture<ProgressComponent>; let fixture: ComponentFixture<ProgressComponent>;
let progressService: ProgressService; let progressService: ProgressService;
let router: MockedRouter;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
RouterTestingModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatIconModule
],
providers: [
ProgressService,
{ provide: Router, useClass: MockedRouter}
], ],
providers: [ ProgressService ],
declarations: [ ProgressComponent ] declarations: [ ProgressComponent ]
}) })
.compileComponents(); .compileComponents();
progressService = TestBed.get(ProgressService); progressService = TestBed.get(ProgressService);
router = TestBed.get(Router);
})); }));
beforeEach(() => { beforeEach(() => {
@ -45,4 +65,21 @@ describe('ProgressComponent', () => {
expect(component.visible).toEqual(false); expect(component.visible).toEqual(false);
}); });
it( 'should set error state when error defined', () => {
const error = new Error("test");
progressService.setError(error);
expect(component.error).toEqual(error);
});
it( 'should clear error when changes route', () => {
const error = new Error("test");
component.error = error;
spyOn(progressService, 'clear');
router.events.next(true);
expect(progressService.clear).toHaveBeenCalled();
});
}); });

View File

@ -1,22 +1,49 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ProgressService } from "./progress.service"; import { ProgressService } from "./progress.service";
import { Router } from "@angular/router";
import { Subscription } from "rxjs/Subscription";
@Component({ @Component({
selector: 'app-progress', selector: 'app-progress',
templateUrl: './progress.component.html', templateUrl: './progress.component.html',
styleUrls: ['./progress.component.scss'] styleUrls: ['./progress.component.scss']
}) })
export class ProgressComponent implements OnInit { export class ProgressComponent implements OnInit, OnDestroy {
visible = false; visible = false;
error: Error;
routerSubscription: Subscription;
constructor( constructor(
private progressService: ProgressService private progressService: ProgressService,
private router: Router,
) { } ) { }
ngOnInit() { ngOnInit() {
this.progressService.state.subscribe((state) => { this.progressService.state.subscribe((state) => {
this.visible = state.visible; this.visible = state.visible;
// only set error state once; ignore next "correct" states
if (state.error && !this.error) {
this.error = state.error;
}
if (state.clear) {
this.error = null;
}
});
// when page changes clear error state
this.routerSubscription = this.router.events.subscribe(() => {
this.progressService.clear();
}); });
} }
refresh() {
// unfortunately we need to use global var
location.reload();
}
ngOnDestroy() {
this.routerSubscription.unsubscribe();
}
} }

View File

@ -1,16 +1,43 @@
import { TestBed, inject } from '@angular/core/testing'; import { TestBed, inject } from '@angular/core/testing';
import { ProgressService } from './progress.service'; import { ProgressService, State } from './progress.service';
describe('ProgressService', () => { describe('ProgressService', () => {
let progressService: ProgressService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ProgressService] providers: [ProgressService]
}); });
progressService = TestBed.get(ProgressService);
spyOn(progressService.state, 'next');
}); });
it('should be created', inject([ProgressService], (service: ProgressService) => { it('should be created', () => {
expect(service).toBeTruthy(); expect(progressService).toBeTruthy();
})); });
it('should propagate event when activated', () => {
progressService.activate();
expect(progressService.state.next).toHaveBeenCalledWith(new State(true));
});
it('should propagate event when deactivated', () => {
progressService.deactivate();
expect(progressService.state.next).toHaveBeenCalledWith(new State(false));
});
it('should propagate event on error', () => {
const error = new Error();
progressService.setError(error);
expect(progressService.state.next).toHaveBeenCalledWith(new State(false, error));
});
it('should clear an error', () => {
progressService.clear();
expect(progressService.state.next).toHaveBeenCalledWith(new State(false, null, true));
});
}); });

View File

@ -5,9 +5,13 @@ import { BehaviorSubject } from "rxjs/BehaviorSubject";
export class State { export class State {
public visible: boolean; public visible: boolean;
public error: Error;
public clear: boolean;
constructor(visible: boolean) { constructor(visible: boolean, error?: Error, clear: boolean = false) {
this.visible = visible; this.visible = visible;
this.error = error;
this.clear = clear;
} }
} }
@ -17,6 +21,14 @@ export class ProgressService {
constructor() {} constructor() {}
public setError(error: Error) {
this.state.next(new State(false, error));
}
public clear() {
this.state.next(new State(false, null, true));
}
public activate() { public activate() {
this.state.next(new State(true)); this.state.next(new State(true));
} }

View File

@ -62,9 +62,6 @@
<app-node-select-interface (onChooseInterface)="onChooseInterface($event)"></app-node-select-interface> <app-node-select-interface (onChooseInterface)="onChooseInterface($event)"></app-node-select-interface>
</div> </div>
<div class="loading-spinner" *ngIf="isLoading"> <app-progress></app-progress>
<mat-spinner color="primary">
</mat-spinner>
</div>
<app-project-map-shortcuts *ngIf="project" [project]="project" [server]="server" [selectionManager]="selectionManager"></app-project-map-shortcuts> <app-project-map-shortcuts *ngIf="project" [project]="project" [server]="server" [selectionManager]="selectionManager"></app-project-map-shortcuts>

View File

@ -41,6 +41,7 @@ import { InRectangleHelper } from "../../cartography/components/map/helpers/in-r
import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource"; import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource";
import { Subscription } from "rxjs/Subscription"; import { Subscription } from "rxjs/Subscription";
import { SettingsService } from "../../services/settings.service"; import { SettingsService } from "../../services/settings.service";
import { ProgressService } from "../../common/progress/progress.service";
@Component({ @Component({
@ -65,8 +66,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
protected selectionManager: SelectionManager; protected selectionManager: SelectionManager;
public isLoading = true;
@ViewChild(MapComponent) mapChild: MapComponent; @ViewChild(MapComponent) mapChild: MapComponent;
@ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent; @ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent;
@ -84,6 +83,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private linkService: LinkService, private linkService: LinkService,
private dialog: MatDialog, private dialog: MatDialog,
private progressDialogService: ProgressDialogService, private progressDialogService: ProgressDialogService,
private progressService: ProgressService,
private toaster: ToasterService, private toaster: ToasterService,
private projectWebServiceHandler: ProjectWebServiceHandler, private projectWebServiceHandler: ProjectWebServiceHandler,
private settingsService: SettingsService, private settingsService: SettingsService,
@ -98,6 +98,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.progressService.activate();
const routeSub = this.route.paramMap.subscribe((paramMap: ParamMap) => { const routeSub = this.route.paramMap.subscribe((paramMap: ParamMap) => {
const server_id = parseInt(paramMap.get('server_id'), 10); const server_id = parseInt(paramMap.get('server_id'), 10);
@ -123,6 +124,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}) })
.subscribe((project: Project) => { .subscribe((project: Project) => {
this.onProjectLoad(project); this.onProjectLoad(project);
}, (error) => {
this.progressService.setError(error);
}, () => {
this.progressService.deactivate();
}); });
}); });
@ -183,7 +188,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.setUpMapCallbacks(project); this.setUpMapCallbacks(project);
this.setUpWS(project); this.setUpWS(project);
this.isLoading = false;
this.progressService.deactivate();
}); });
this.subscriptions.push(subscription); this.subscriptions.push(subscription);
} }
@ -335,7 +341,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.nodesDataSource.clear(); this.nodesDataSource.clear();
this.linksDataSource.clear(); this.linksDataSource.clear();
this.ws.unsubscribe(); if (this.ws) {
this.ws.unsubscribe();
}
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
} }

View File

@ -62,6 +62,8 @@ export class ProjectsComponent implements OnInit {
.list(this.server) .list(this.server)
.subscribe((projects: Project[]) => { .subscribe((projects: Project[]) => {
this.projectDatabase.addProjects(projects); this.projectDatabase.addProjects(projects);
}, (error) => {
this.progressService.setError(error);
}); });
} }