mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	better toasts, better dashboard, first implementation of consumer status
This commit is contained in:
		| @@ -1,14 +1,43 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { ConsumerStatusService } from './services/consumer-status.service'; | ||||
| import { Toast, ToastService } from './services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
|   templateUrl: './app.component.html', | ||||
|   styleUrls: ['./app.component.css'] | ||||
| }) | ||||
| export class AppComponent { | ||||
| export class AppComponent implements OnInit, OnDestroy { | ||||
|  | ||||
|   successSubscription: Subscription; | ||||
|   failedSubscription: Subscription; | ||||
|    | ||||
|   constructor () { | ||||
|   constructor ( private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router ) { | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
|     this.consumerStatusService.disconnect() | ||||
|     this.successSubscription.unsubscribe() | ||||
|     this.failedSubscription.unsubscribe() | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.consumerStatusService.connect() | ||||
|  | ||||
|     this.successSubscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => { | ||||
|       this.toastService.showToast({title: "Document added", content: `Document ${status.filename} was added to paperless.`, actionName: "Open document", action: () => { | ||||
|         this.router.navigate(['documents', status.document_id]) | ||||
|       }}) | ||||
|     }) | ||||
|  | ||||
|     this.failedSubscription = this.consumerStatusService.onDocumentConsumptionFailed().subscribe(status => { | ||||
|       this.toastService.showError(`Could not consume ${status.filename}: ${status.message}`) | ||||
|     }) | ||||
|  | ||||
|   } | ||||
|  | ||||
|    | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,10 @@ import { SaveViewConfigDialogComponent } from './components/document-list/save-v | ||||
| import { InfiniteScrollModule } from 'ngx-infinite-scroll'; | ||||
| import { DateTimeComponent } from './components/common/input/date-time/date-time.component'; | ||||
| import { TagsComponent } from './components/common/input/tags/tags.component'; | ||||
| import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'; | ||||
| import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component'; | ||||
| import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'; | ||||
| import { FileUploadWidgetComponent } from './components/dashboard/widgets/file-upload-widget/file-upload-widget.component'; | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
| @@ -73,7 +77,11 @@ import { TagsComponent } from './components/common/input/tags/tags.component'; | ||||
|     CheckComponent, | ||||
|     SaveViewConfigDialogComponent, | ||||
|     DateTimeComponent, | ||||
|     TagsComponent | ||||
|     TagsComponent, | ||||
|     ConsumerStatusWidgetComponent, | ||||
|     SavedViewWidgetComponent, | ||||
|     StatisticsWidgetComponent, | ||||
|     FileUploadWidgetComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; | ||||
| import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model'; | ||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Directive() | ||||
| export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit { | ||||
| @@ -66,7 +66,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI | ||||
|       this.activeModal.close() | ||||
|       this.success.emit(result) | ||||
|     }, error => { | ||||
|       this.toastService.showToast(Toast.makeError(`Could not save ${this.entityName}: ${error.error.name}`)) | ||||
|       this.toastService.showError(`Could not save ${this.entityName}: ${error.error.name}`) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| <ngb-toast | ||||
|   *ngFor="let toast of toasts" | ||||
|   [header]="toast.title" [autohide]="true" [delay]="toast.delay" | ||||
|   [header]="toast.title" [autohide]="true" [delay]="toast.delay || 5000" | ||||
|   [class]="toast.classname" | ||||
|   (hide)="toastService.closeToast(toast)"> | ||||
|   {{toast.content}} | ||||
|   <p>{{toast.content}}</p> | ||||
|   <p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> | ||||
| </ngb-toast> | ||||
| @@ -6,59 +6,11 @@ | ||||
|  | ||||
| <div class='row'> | ||||
|   <div class="col-lg"> | ||||
|     <ng-container *ngFor="let v of savedDashboardViews"> | ||||
|       <h4>{{v.viewConfig.title}}</h4> | ||||
|  | ||||
|       <table class="table table-sm table-hover table-borderless"> | ||||
|         <thead> | ||||
|         <tr> | ||||
|           <th>Date created</th> | ||||
|           <th scope="col">Document</th> | ||||
|         </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr *ngFor="let doc of v.documents" routerLink="/documents/{{doc.id}}"> | ||||
|             <td>{{doc.created | date}}</td> | ||||
|             <td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags" class="ml-1"></app-tag> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|  | ||||
|     </ng-container> | ||||
|     <ng-container *ngIf="savedDashboardViews.length == 0"> | ||||
|       <h4>Saved views</h4> | ||||
|       <p>This space is reserved to display your saved views. Go to your documents and save a view to have it displayed here!</p> | ||||
|     </ng-container> | ||||
|  | ||||
|     <app-saved-view-widget [viewConfig]="conf" *ngFor="let conf of savedViewConfigService.getDashboardConfigs()"></app-saved-view-widget> | ||||
|   </div> | ||||
|   <div class="col-lg"> | ||||
|     <h4>Statistics</h4> | ||||
|     <p>Documents in inbox: {{statistics.documents_inbox}}</p> | ||||
|     <p>Total documents: {{statistics.documents_total}}</p> | ||||
|     <h4>Upload new Document</h4> | ||||
|     <form> | ||||
|       <ngx-file-drop | ||||
|         dropZoneLabel="Drop documents here" | ||||
|         (onFileDrop)="dropped($event)" | ||||
|         (onFileOver)="fileOver($event)" | ||||
|         (onFileLeave)="fileLeave($event)" | ||||
|         dropZoneClassName="bg-light mt-4 card"> | ||||
|  | ||||
|       </ngx-file-drop> | ||||
|     </form> | ||||
|     <h5 class="mt-3">Document conumser status</h5> | ||||
|     <p>This is what it might look like in the future.</p> | ||||
|     <div class="card bg-light mb-2"> | ||||
|       <div class="card-body"> | ||||
|         <p class="card-text"><strong>Filename.pdf:</strong> Running tesseract on page 4/8...</p> | ||||
|         <p><ngb-progressbar type="info" [value]="50"></ngb-progressbar></p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="card bg-light mb-2"> | ||||
|       <div class="card-body"> | ||||
|         <p class="card-text"><strong>Filename2.pdf:</strong> Completed.</p> | ||||
|         <p><ngb-progressbar type="success" [value]="100"></ngb-progressbar></p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <app-statistics-widget></app-statistics-widget> | ||||
|     <app-file-upload-widget></app-file-upload-widget> | ||||
|     <app-consumer-status-widget></app-consumer-status-widget> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -4,14 +4,9 @@ import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
|  | ||||
| export interface Statistics { | ||||
|   documents_total?: number | ||||
|   documents_inbox?: number | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-dashboard', | ||||
|   templateUrl: './dashboard.component.html', | ||||
| @@ -19,53 +14,9 @@ export interface Statistics { | ||||
| }) | ||||
| export class DashboardComponent implements OnInit { | ||||
|  | ||||
|   constructor(private documentService: DocumentService, private toastService: ToastService, | ||||
|     public savedViewConfigService: SavedViewConfigService, private http: HttpClient) { } | ||||
|  | ||||
|  | ||||
|   savedDashboardViews = [] | ||||
|   statistics: Statistics = {} | ||||
|   constructor(public savedViewConfigService: SavedViewConfigService) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.savedViewConfigService.getDashboardConfigs().forEach(config => { | ||||
|       this.documentService.list(1,10,config.sortField,config.sortDirection,config.filterRules).subscribe(result => { | ||||
|         this.savedDashboardViews.push({viewConfig: config, documents: result.results}) | ||||
|       }) | ||||
|     }) | ||||
|     this.getStatistics().subscribe(statistics => { | ||||
|       this.statistics = statistics | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   getStatistics(): Observable<Statistics> { | ||||
|     return this.http.get(`${environment.apiBaseUrl}statistics/`) | ||||
|   } | ||||
|  | ||||
|  | ||||
|   public fileOver(event){ | ||||
|     console.log(event); | ||||
|   } | ||||
|   | ||||
|   public fileLeave(event){ | ||||
|     console.log(event); | ||||
|   } | ||||
|   | ||||
|   public dropped(files: NgxFileDropEntry[]) { | ||||
|     for (const droppedFile of files) { | ||||
|       if (droppedFile.fileEntry.isFile) { | ||||
|         const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; | ||||
|         console.log(fileEntry) | ||||
|         fileEntry.file((file: File) => { | ||||
|           console.log(file) | ||||
|           const formData = new FormData() | ||||
|           formData.append('document', file, file.name) | ||||
|           this.documentService.uploadDocument(formData).subscribe(result => { | ||||
|             this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly.")) | ||||
|           }, error => { | ||||
|             this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!")) | ||||
|           }) | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| <h4 class="mt-3">Document consumer status</h4> | ||||
|  | ||||
| <div class="p-2 mb-1 border bg-light" *ngFor="let s of getStatus()"> | ||||
|   <div class="mb-1"><strong>{{s.filename}}:</strong> {{s.message}}</div> | ||||
|   <ngb-progressbar [type]="getType(s.status)" [value]="s.current_progress" [max]="s.max_progress" class="mb-2"></ngb-progressbar> | ||||
|   <div *ngIf="isFinished(s)"> | ||||
|     <button *ngIf="s.document_id" class="btn btn-sm btn-outline-secondary mr-2" routerLink="/documents/{{s.document_id}}" (click)="dismiss(s)">Open document</button> | ||||
|     <button class="btn btn-sm btn-outline-secondary" (click)="dismiss(s)">Dismiss</button> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ConsumerStatusWidgetComponent } from './consumer-status-widget.component'; | ||||
|  | ||||
| describe('ConsumerStatusWidgetComponent', () => { | ||||
|   let component: ConsumerStatusWidgetComponent; | ||||
|   let fixture: ComponentFixture<ConsumerStatusWidgetComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ConsumerStatusWidgetComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ConsumerStatusWidgetComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,35 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { ConsumerStatusService, FileStatus } from 'src/app/services/consumer-status.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-consumer-status-widget', | ||||
|   templateUrl: './consumer-status-widget.component.html', | ||||
|   styleUrls: ['./consumer-status-widget.component.css'] | ||||
| }) | ||||
| export class ConsumerStatusWidgetComponent implements OnInit { | ||||
|  | ||||
|   constructor(private consumerStatusService: ConsumerStatusService) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   getStatus() { | ||||
|     return this.consumerStatusService.consumerStatus | ||||
|   } | ||||
|  | ||||
|   isFinished(status: FileStatus) { | ||||
|     return status.status == "FAILED" || status.status == "SUCCESS" | ||||
|   } | ||||
|  | ||||
|   getType(status) { | ||||
|     switch (status) { | ||||
|       case "WORKING": return "primary" | ||||
|       case "FAILED": return "danger" | ||||
|       case "SUCCESS": return "success" | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   dismiss(status: FileStatus) { | ||||
|     this.consumerStatusService.dismiss(status) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| <h4>Upload new Document</h4> | ||||
| <form> | ||||
|   <ngx-file-drop | ||||
|     dropZoneLabel="Drop documents here" | ||||
|     (onFileDrop)="dropped($event)" | ||||
|     (onFileOver)="fileOver($event)" | ||||
|     (onFileLeave)="fileLeave($event)" | ||||
|     dropZoneClassName="bg-light mt-4 card"> | ||||
|  | ||||
|   </ngx-file-drop> | ||||
| </form> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { FileUploadWidgetComponent } from './file-upload-widget.component'; | ||||
|  | ||||
| describe('FileUploadWidgetComponent', () => { | ||||
|   let component: FileUploadWidgetComponent; | ||||
|   let fixture: ComponentFixture<FileUploadWidgetComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ FileUploadWidgetComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(FileUploadWidgetComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,44 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-file-upload-widget', | ||||
|   templateUrl: './file-upload-widget.component.html', | ||||
|   styleUrls: ['./file-upload-widget.component.css'] | ||||
| }) | ||||
| export class FileUploadWidgetComponent implements OnInit { | ||||
|  | ||||
|   constructor(private documentService: DocumentService, private toastService: ToastService) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   public fileOver(event){ | ||||
|     console.log(event); | ||||
|   } | ||||
|   | ||||
|   public fileLeave(event){ | ||||
|     console.log(event); | ||||
|   } | ||||
|   | ||||
|   public dropped(files: NgxFileDropEntry[]) { | ||||
|     for (const droppedFile of files) { | ||||
|       if (droppedFile.fileEntry.isFile) { | ||||
|         const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; | ||||
|         console.log(fileEntry) | ||||
|         fileEntry.file((file: File) => { | ||||
|           console.log(file) | ||||
|           const formData = new FormData() | ||||
|           formData.append('document', file, file.name) | ||||
|           this.documentService.uploadDocument(formData).subscribe(result => { | ||||
|             this.toastService.showInfo("The document has been uploaded and will be processed by the consumer shortly.") | ||||
|           }, error => { | ||||
|             this.toastService.showError("An error has occured while uploading the document. Sorry!") | ||||
|           }) | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| <h4>{{viewConfig.title}}</h4> | ||||
|  | ||||
| <table class="table table-sm table-hover table-borderless"> | ||||
|   <thead> | ||||
|   <tr> | ||||
|     <th>Date created</th> | ||||
|     <th scope="col">Document</th> | ||||
|   </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}"> | ||||
|       <td>{{doc.created | date}}</td> | ||||
|       <td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags" class="ml-1"></app-tag> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { SavedViewWidgetComponent } from './saved-view-widget.component'; | ||||
|  | ||||
| describe('SavedViewWidgetComponent', () => { | ||||
|   let component: SavedViewWidgetComponent; | ||||
|   let fixture: ComponentFixture<SavedViewWidgetComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ SavedViewWidgetComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(SavedViewWidgetComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,41 @@ | ||||
| import { Component, Input, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { SavedViewConfig } from 'src/app/data/saved-view-config'; | ||||
| import { ConsumerStatusService } from 'src/app/services/consumer-status.service'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-saved-view-widget', | ||||
|   templateUrl: './saved-view-widget.component.html', | ||||
|   styleUrls: ['./saved-view-widget.component.css'] | ||||
| }) | ||||
| export class SavedViewWidgetComponent implements OnInit, OnDestroy { | ||||
|  | ||||
|   constructor(private documentService: DocumentService, private consumerStatusService: ConsumerStatusService) { } | ||||
|  | ||||
|   @Input() | ||||
|   viewConfig: SavedViewConfig | ||||
|  | ||||
|   documents: PaperlessDocument[] | ||||
|  | ||||
|   subscription: Subscription | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.reload() | ||||
|     this.subscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => { | ||||
|       this.reload() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscription.unsubscribe() | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
|     this.documentService.list(1,10,this.viewConfig.sortField,this.viewConfig.sortDirection,this.viewConfig.filterRules).subscribe(result => { | ||||
|       this.documents = result.results | ||||
|     }) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| <h4>Statistics</h4> | ||||
| <p>Documents in inbox: {{statistics.documents_inbox}}</p> | ||||
| <p>Total documents: {{statistics.documents_total}}</p> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { StatisticsWidgetComponent } from './statistics-widget.component'; | ||||
|  | ||||
| describe('StatisticsWidgetComponent', () => { | ||||
|   let component: StatisticsWidgetComponent; | ||||
|   let fixture: ComponentFixture<StatisticsWidgetComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ StatisticsWidgetComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(StatisticsWidgetComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,32 @@ | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
|  | ||||
| export interface Statistics { | ||||
|   documents_total?: number | ||||
|   documents_inbox?: number | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-statistics-widget', | ||||
|   templateUrl: './statistics-widget.component.html', | ||||
|   styleUrls: ['./statistics-widget.component.css'] | ||||
| }) | ||||
| export class StatisticsWidgetComponent implements OnInit { | ||||
|  | ||||
|   constructor(private http: HttpClient) { } | ||||
|  | ||||
|   statistics: Statistics = {} | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.getStatistics().subscribe(statistics => { | ||||
|       this.statistics = statistics | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   getStatistics(): Observable<Statistics> { | ||||
|     return this.http.get(`${environment.apiBaseUrl}statistics/`) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { AuthService } from 'src/app/services/auth.service'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-login', | ||||
| @@ -26,7 +26,7 @@ export class LoginComponent implements OnInit { | ||||
|     this.auth.login(this.loginForm.value.username, this.loginForm.value.password, this.loginForm.value.rememberMe).subscribe(result => { | ||||
|       this.router.navigate(['']) | ||||
|     }, (error) => { | ||||
|       this.toastService.showToast(Toast.makeError("Unable to log in with provided credentials.")) | ||||
|       this.toastService.showError("Unable to log in with provided credentials.") | ||||
|     } | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -28,7 +28,7 @@ export class AuthInterceptor implements HttpInterceptor { | ||||
|       catchError((error: HttpErrorResponse) => { | ||||
|         if (error.status == 401 && this.authService.isAuthenticated()) { | ||||
|           this.authService.logout() | ||||
|           this.toastService.showToast(Toast.makeError("Your session has expired. Please log in again.")) | ||||
|           this.toastService.showError("Your session has expired. Please log in again.") | ||||
|         } | ||||
|         return throwError(error) | ||||
|       }) | ||||
|   | ||||
							
								
								
									
										16
									
								
								src-ui/src/app/services/consumer-status.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src-ui/src/app/services/consumer-status.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import { TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ConsumerStatusService } from './consumer-status.service'; | ||||
|  | ||||
| describe('ConsumerStatusService', () => { | ||||
|   let service: ConsumerStatusService; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({}); | ||||
|     service = TestBed.inject(ConsumerStatusService); | ||||
|   }); | ||||
|  | ||||
|   it('should be created', () => { | ||||
|     expect(service).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										71
									
								
								src-ui/src/app/services/consumer-status.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src-ui/src/app/services/consumer-status.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Subject } from 'rxjs'; | ||||
|  | ||||
| export interface FileStatus { | ||||
|   filename?: string | ||||
|   current_progress?: number | ||||
|   max_progress?: number | ||||
|   status?: string | ||||
|   message?: string | ||||
|   document_id?: number | ||||
| } | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class ConsumerStatusService { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   private statusWebSocked: WebSocket | ||||
|  | ||||
|   consumerStatus: FileStatus[] = [] | ||||
|   private documentConsumptionFinishedSubject = new Subject<FileStatus>() | ||||
|   private documentConsumptionFailedSubject = new Subject<FileStatus>() | ||||
|  | ||||
|   connect() { | ||||
|     this.disconnect() | ||||
|     this.statusWebSocked = new WebSocket("ws://localhost:8000/ws/status/"); | ||||
|     this.statusWebSocked.onmessage = (ev) => { | ||||
|       let statusUpdate: FileStatus = JSON.parse(ev['data']) | ||||
|  | ||||
|       let index = this.consumerStatus.findIndex(fs => fs.filename == statusUpdate.filename) | ||||
|       if (index > -1) { | ||||
|         this.consumerStatus[index] = statusUpdate | ||||
|       } else { | ||||
|         this.consumerStatus.push(statusUpdate) | ||||
|       } | ||||
|  | ||||
|       if (statusUpdate.status == "SUCCESS") { | ||||
|         this.documentConsumptionFinishedSubject.next(statusUpdate) | ||||
|       } | ||||
|       if (statusUpdate.status == "FAILED") { | ||||
|         this.documentConsumptionFailedSubject.next(statusUpdate) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   disconnect() { | ||||
|     if (this.statusWebSocked) { | ||||
|       this.statusWebSocked.close() | ||||
|       this.statusWebSocked = null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   dismiss(status: FileStatus) { | ||||
|     let index = this.consumerStatus.findIndex(s => s.filename == status.filename) | ||||
|  | ||||
|     if (index > -1) { | ||||
|       this.consumerStatus.splice(index, 1) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   onDocumentConsumptionFinished() { | ||||
|     return this.documentConsumptionFinishedSubject | ||||
|   } | ||||
|  | ||||
|   onDocumentConsumptionFailed() { | ||||
|     return this.documentConsumptionFailedSubject | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,30 +1,17 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Subject, zip } from 'rxjs'; | ||||
|  | ||||
| export class Toast { | ||||
|  | ||||
|   static make(title: string, content: string, classname?: string, delay?: number): Toast { | ||||
|     let t = new Toast() | ||||
|     t.title = title | ||||
|     t.content = content | ||||
|     t.classname = classname | ||||
|     if (delay) { | ||||
|       t.delay = delay | ||||
|     } | ||||
|     return t | ||||
|   } | ||||
|  | ||||
|   static makeError(content: string) { | ||||
|     return Toast.make("Error", content, null, 10000) | ||||
|   } | ||||
| export interface Toast { | ||||
|  | ||||
|   title: string | ||||
|  | ||||
|   classname: string | ||||
|  | ||||
|   content: string | ||||
|  | ||||
|   delay: number = 5000 | ||||
|   delay?: number | ||||
|  | ||||
|   action?: any | ||||
|  | ||||
|   actionName?: string | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -44,6 +31,14 @@ export class ToastService { | ||||
|     this.toastsSubject.next(this.toasts) | ||||
|   } | ||||
|  | ||||
|   showInfo(message: string) { | ||||
|     this.showToast({title: "Information", content: message, delay: 5000}) | ||||
|   } | ||||
|  | ||||
|   showError(message: string) { | ||||
|     this.showToast({title: "Error", content: message, delay: 10000}) | ||||
|   } | ||||
|  | ||||
|   closeToast(toast: Toast) { | ||||
|     let index = this.toasts.findIndex(t => t == toast) | ||||
|     if (index > -1) { | ||||
|   | ||||
| @@ -4,7 +4,8 @@ | ||||
|  | ||||
| export const environment = { | ||||
|   production: false, | ||||
|   apiBaseUrl: "http://localhost:8000/api/" | ||||
|   apiBaseUrl: "http://localhost:8000/api/", | ||||
|   wsBaseUrl: "ws://localhost:8000/ws/" | ||||
| }; | ||||
|  | ||||
| /* | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jonas Winkler
					Jonas Winkler