diff --git a/src/app/common/progress/progress.component.html b/src/app/common/progress/progress.component.html index c240c710..9778c6d6 100644 --- a/src/app/common/progress/progress.component.html +++ b/src/app/common/progress/progress.component.html @@ -1,8 +1,20 @@ -
-
+
+
+
+
error_outline
+
Error occurred: {{ error.message }}
+
+ + +
+
diff --git a/src/app/common/progress/progress.component.scss b/src/app/common/progress/progress.component.scss index 8bfff484..ec3a3ecf 100644 --- a/src/app/common/progress/progress.component.scss +++ b/src/app/common/progress/progress.component.scss @@ -10,9 +10,19 @@ z-index: 1000; } -.loading-spinner { +.loading-spinner, .error-state { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); } + +.error-state div { + text-align: center; +} + +.error-icon mat-icon { + font-size: 64px; + width: 64px; + height: 64px; +} diff --git a/src/app/common/progress/progress.component.spec.ts b/src/app/common/progress/progress.component.spec.ts index 5780e205..f0cfc8b2 100644 --- a/src/app/common/progress/progress.component.spec.ts +++ b/src/app/common/progress/progress.component.spec.ts @@ -1,25 +1,45 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ProgressComponent } from './progress.component'; -import { MatProgressSpinnerModule } from "@angular/material"; +import { MatIconModule, MatProgressSpinnerModule } from "@angular/material"; 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; + + constructor() { + this.events = new BehaviorSubject(true); + } +} + describe('ProgressComponent', () => { let component: ProgressComponent; let fixture: ComponentFixture; let progressService: ProgressService; + let router: MockedRouter; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ + RouterTestingModule, MatProgressSpinnerModule, + MatIconModule + ], + providers: [ + ProgressService, + { provide: Router, useClass: MockedRouter} ], - providers: [ ProgressService ], declarations: [ ProgressComponent ] }) .compileComponents(); progressService = TestBed.get(ProgressService); + router = TestBed.get(Router); })); beforeEach(() => { @@ -45,4 +65,21 @@ describe('ProgressComponent', () => { 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(); + }); + }); diff --git a/src/app/common/progress/progress.component.ts b/src/app/common/progress/progress.component.ts index 3a823ad2..cd4f2607 100644 --- a/src/app/common/progress/progress.component.ts +++ b/src/app/common/progress/progress.component.ts @@ -1,22 +1,49 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { ProgressService } from "./progress.service"; +import { Router } from "@angular/router"; +import { Subscription } from "rxjs/Subscription"; @Component({ selector: 'app-progress', templateUrl: './progress.component.html', styleUrls: ['./progress.component.scss'] }) -export class ProgressComponent implements OnInit { +export class ProgressComponent implements OnInit, OnDestroy { visible = false; + error: Error; + routerSubscription: Subscription; constructor( - private progressService: ProgressService + private progressService: ProgressService, + private router: Router, ) { } ngOnInit() { this.progressService.state.subscribe((state) => { 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(); + } } diff --git a/src/app/common/progress/progress.service.spec.ts b/src/app/common/progress/progress.service.spec.ts index dc375e57..530216ff 100644 --- a/src/app/common/progress/progress.service.spec.ts +++ b/src/app/common/progress/progress.service.spec.ts @@ -1,16 +1,43 @@ import { TestBed, inject } from '@angular/core/testing'; -import { ProgressService } from './progress.service'; +import { ProgressService, State } from './progress.service'; describe('ProgressService', () => { + let progressService: ProgressService; + beforeEach(() => { TestBed.configureTestingModule({ providers: [ProgressService] }); + + progressService = TestBed.get(ProgressService); + + spyOn(progressService.state, 'next'); }); - it('should be created', inject([ProgressService], (service: ProgressService) => { - expect(service).toBeTruthy(); - })); + it('should be created', () => { + 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)); + }); }); diff --git a/src/app/common/progress/progress.service.ts b/src/app/common/progress/progress.service.ts index 1ac037a0..4c68d3c0 100644 --- a/src/app/common/progress/progress.service.ts +++ b/src/app/common/progress/progress.service.ts @@ -5,9 +5,13 @@ import { BehaviorSubject } from "rxjs/BehaviorSubject"; export class State { public visible: boolean; + public error: Error; + public clear: boolean; - constructor(visible: boolean) { + constructor(visible: boolean, error?: Error, clear: boolean = false) { this.visible = visible; + this.error = error; + this.clear = clear; } } @@ -17,6 +21,14 @@ export class ProgressService { constructor() {} + public setError(error: Error) { + this.state.next(new State(false, error)); + } + + public clear() { + this.state.next(new State(false, null, true)); + } + public activate() { this.state.next(new State(true)); } diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 63962683..d679d016 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -62,9 +62,6 @@
-
- - -
+ diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index f65b88f0..19d1537b 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -41,6 +41,7 @@ import { InRectangleHelper } from "../../cartography/components/map/helpers/in-r import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource"; import { Subscription } from "rxjs/Subscription"; import { SettingsService } from "../../services/settings.service"; +import { ProgressService } from "../../common/progress/progress.service"; @Component({ @@ -65,8 +66,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy { protected selectionManager: SelectionManager; - public isLoading = true; - @ViewChild(MapComponent) mapChild: MapComponent; @ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent; @@ -84,6 +83,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private linkService: LinkService, private dialog: MatDialog, private progressDialogService: ProgressDialogService, + private progressService: ProgressService, private toaster: ToasterService, private projectWebServiceHandler: ProjectWebServiceHandler, private settingsService: SettingsService, @@ -98,6 +98,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { } ngOnInit() { + this.progressService.activate(); const routeSub = this.route.paramMap.subscribe((paramMap: ParamMap) => { const server_id = parseInt(paramMap.get('server_id'), 10); @@ -123,6 +124,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }) .subscribe((project: 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.setUpWS(project); - this.isLoading = false; + + this.progressService.deactivate(); }); this.subscriptions.push(subscription); } @@ -335,7 +341,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.nodesDataSource.clear(); this.linksDataSource.clear(); - this.ws.unsubscribe(); + if (this.ws) { + this.ws.unsubscribe(); + } this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } diff --git a/src/app/components/projects/projects.component.ts b/src/app/components/projects/projects.component.ts index 47c327c7..bb5d6a95 100644 --- a/src/app/components/projects/projects.component.ts +++ b/src/app/components/projects/projects.component.ts @@ -62,6 +62,8 @@ export class ProjectsComponent implements OnInit { .list(this.server) .subscribe((projects: Project[]) => { this.projectDatabase.addProjects(projects); + }, (error) => { + this.progressService.setError(error); }); }