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({ | @Component({ | ||||||
|   selector: 'app-root', |   selector: 'app-root', | ||||||
|   templateUrl: './app.component.html', |   templateUrl: './app.component.html', | ||||||
|   styleUrls: ['./app.component.css'] |   styleUrls: ['./app.component.css'] | ||||||
| }) | }) | ||||||
| export class AppComponent { | export class AppComponent implements OnInit, OnDestroy { | ||||||
|  |  | ||||||
|  |   successSubscription: Subscription; | ||||||
|  |   failedSubscription: Subscription; | ||||||
|  |    | ||||||
|  |   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}`) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|   constructor () { |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|    |    | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,6 +40,10 @@ import { SaveViewConfigDialogComponent } from './components/document-list/save-v | |||||||
| import { InfiniteScrollModule } from 'ngx-infinite-scroll'; | import { InfiniteScrollModule } from 'ngx-infinite-scroll'; | ||||||
| import { DateTimeComponent } from './components/common/input/date-time/date-time.component'; | import { DateTimeComponent } from './components/common/input/date-time/date-time.component'; | ||||||
| import { TagsComponent } from './components/common/input/tags/tags.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({ | @NgModule({ | ||||||
|   declarations: [ |   declarations: [ | ||||||
| @@ -73,7 +77,11 @@ import { TagsComponent } from './components/common/input/tags/tags.component'; | |||||||
|     CheckComponent, |     CheckComponent, | ||||||
|     SaveViewConfigDialogComponent, |     SaveViewConfigDialogComponent, | ||||||
|     DateTimeComponent, |     DateTimeComponent, | ||||||
|     TagsComponent |     TagsComponent, | ||||||
|  |     ConsumerStatusWidgetComponent, | ||||||
|  |     SavedViewWidgetComponent, | ||||||
|  |     StatisticsWidgetComponent, | ||||||
|  |     FileUploadWidgetComponent | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; | |||||||
| import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model'; | import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model'; | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | 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() | @Directive() | ||||||
| export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit { | 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.activeModal.close() | ||||||
|       this.success.emit(result) |       this.success.emit(result) | ||||||
|     }, error => { |     }, 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 | <ngb-toast | ||||||
|   *ngFor="let toast of toasts" |   *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" |   [class]="toast.classname" | ||||||
|   (hide)="toastService.closeToast(toast)"> |   (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> | </ngb-toast> | ||||||
| @@ -6,59 +6,11 @@ | |||||||
|  |  | ||||||
| <div class='row'> | <div class='row'> | ||||||
|   <div class="col-lg"> |   <div class="col-lg"> | ||||||
|     <ng-container *ngFor="let v of savedDashboardViews"> |     <app-saved-view-widget [viewConfig]="conf" *ngFor="let conf of savedViewConfigService.getDashboardConfigs()"></app-saved-view-widget> | ||||||
|       <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> |  | ||||||
|  |  | ||||||
|   </div> |   </div> | ||||||
|   <div class="col-lg"> |   <div class="col-lg"> | ||||||
|     <h4>Statistics</h4> |     <app-statistics-widget></app-statistics-widget> | ||||||
|     <p>Documents in inbox: {{statistics.documents_inbox}}</p> |     <app-file-upload-widget></app-file-upload-widget> | ||||||
|     <p>Total documents: {{statistics.documents_total}}</p> |     <app-consumer-status-widget></app-consumer-status-widget> | ||||||
|     <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> |  | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -4,14 +4,9 @@ import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'; | |||||||
| import { Observable } from 'rxjs'; | import { Observable } from 'rxjs'; | ||||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | import { DocumentService } from 'src/app/services/rest/document.service'; | ||||||
| import { SavedViewConfigService } from 'src/app/services/saved-view-config.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'; | import { environment } from 'src/environments/environment'; | ||||||
|  |  | ||||||
| export interface Statistics { |  | ||||||
|   documents_total?: number |  | ||||||
|   documents_inbox?: number |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-dashboard', |   selector: 'app-dashboard', | ||||||
|   templateUrl: './dashboard.component.html', |   templateUrl: './dashboard.component.html', | ||||||
| @@ -19,53 +14,9 @@ export interface Statistics { | |||||||
| }) | }) | ||||||
| export class DashboardComponent implements OnInit { | export class DashboardComponent implements OnInit { | ||||||
|  |  | ||||||
|   constructor(private documentService: DocumentService, private toastService: ToastService, |   constructor(public savedViewConfigService: SavedViewConfigService) { } | ||||||
|     public savedViewConfigService: SavedViewConfigService, private http: HttpClient) { } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   savedDashboardViews = [] |  | ||||||
|   statistics: Statistics = {} |  | ||||||
|  |  | ||||||
|   ngOnInit(): void { |   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 { FormControl, FormGroup } from '@angular/forms'; | ||||||
| import { Router } from '@angular/router'; | import { Router } from '@angular/router'; | ||||||
| import { AuthService } from 'src/app/services/auth.service'; | 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({ | @Component({ | ||||||
|   selector: 'app-login', |   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.auth.login(this.loginForm.value.username, this.loginForm.value.password, this.loginForm.value.rememberMe).subscribe(result => { | ||||||
|       this.router.navigate(['']) |       this.router.navigate(['']) | ||||||
|     }, (error) => { |     }, (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) => { |       catchError((error: HttpErrorResponse) => { | ||||||
|         if (error.status == 401 && this.authService.isAuthenticated()) { |         if (error.status == 401 && this.authService.isAuthenticated()) { | ||||||
|           this.authService.logout() |           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) |         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 { Injectable } from '@angular/core'; | ||||||
| import { Subject, zip } from 'rxjs'; | import { Subject, zip } from 'rxjs'; | ||||||
|  |  | ||||||
| export class Toast { | export interface 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) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   title: string |   title: string | ||||||
|  |  | ||||||
|   classname: string |  | ||||||
|  |  | ||||||
|   content: string |   content: string | ||||||
|  |  | ||||||
|   delay: number = 5000 |   delay?: number | ||||||
|  |  | ||||||
|  |   action?: any | ||||||
|  |  | ||||||
|  |   actionName?: string | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -44,6 +31,14 @@ export class ToastService { | |||||||
|     this.toastsSubject.next(this.toasts) |     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) { |   closeToast(toast: Toast) { | ||||||
|     let index = this.toasts.findIndex(t => t == toast) |     let index = this.toasts.findIndex(t => t == toast) | ||||||
|     if (index > -1) { |     if (index > -1) { | ||||||
|   | |||||||
| @@ -4,7 +4,8 @@ | |||||||
|  |  | ||||||
| export const environment = { | export const environment = { | ||||||
|   production: false, |   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