mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-11 10:00:48 -05:00
better toasts, better dashboard, first implementation of consumer status
This commit is contained in:
parent
572e40ca27
commit
036f11acaa
@ -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/"
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user