mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06: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