mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Frontend better handle slow backend requests
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
|   <div class="btn-group w-100" ngbDropdown role="group"> | ||||
|   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'"> | ||||
|   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> | ||||
|     {{title}} | ||||
|     <app-clearable-badge [selected]="isActive" (cleared)="reset()"></app-clearable-badge><span class="visually-hidden">selected</span> | ||||
|   </button> | ||||
|   | ||||
| @@ -85,6 +85,9 @@ export class DateDropdownComponent implements OnInit, OnDestroy { | ||||
|   @Output() | ||||
|   datesSet = new EventEmitter<DateSelection>() | ||||
|  | ||||
|   @Input() | ||||
|   disabled: boolean = false | ||||
|  | ||||
|   get isActive(): boolean { | ||||
|     return ( | ||||
|       this.relativeDate !== null || | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <div class="btn-group w-100" ngbDropdown role="group"> | ||||
|     <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'"> | ||||
|     <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> | ||||
|         <svg class="toolbaricon" fill="currentColor"> | ||||
|            <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> | ||||
|         </svg> | ||||
|   | ||||
| @@ -4,11 +4,10 @@ import { | ||||
|   OnDestroy, | ||||
|   OnInit, | ||||
|   QueryList, | ||||
|   ViewChild, | ||||
|   ViewChildren, | ||||
| } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { Subscription } from 'rxjs' | ||||
| import { Subject, takeUntil } from 'rxjs' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||
| import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | ||||
| @@ -49,7 +48,7 @@ export class SavedViewWidgetComponent | ||||
|  | ||||
|   documents: PaperlessDocument[] = [] | ||||
|  | ||||
|   subscription: Subscription | ||||
|   unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   @ViewChildren('popover') popovers: QueryList<NgbPopover> | ||||
|   popover: NgbPopover | ||||
| @@ -59,15 +58,17 @@ export class SavedViewWidgetComponent | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.reload() | ||||
|     this.subscription = this.consumerStatusService | ||||
|     this.consumerStatusService | ||||
|       .onDocumentConsumptionFinished() | ||||
|       .subscribe((status) => { | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         this.reload() | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscription.unsubscribe() | ||||
|     this.unsubscribeNotifier.next(true) | ||||
|     this.unsubscribeNotifier.complete() | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
| @@ -81,6 +82,7 @@ export class SavedViewWidgetComponent | ||||
|         this.savedView.filter_rules, | ||||
|         { truncate_content: true } | ||||
|       ) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((result) => { | ||||
|         this.loading = false | ||||
|         this.documents = result.results | ||||
|   | ||||
| @@ -185,7 +185,7 @@ export class DocumentListComponent | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
|     // unsubscribes all | ||||
|     this.list.cancelPending() | ||||
|     this.unsubscribeNotifier.next(this) | ||||
|     this.unsubscribeNotifier.complete() | ||||
|   } | ||||
|   | ||||
| @@ -2,16 +2,23 @@ | ||||
|  | ||||
| </app-page-header> | ||||
|  | ||||
|  | ||||
| <ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs"> | ||||
|   <li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile"> | ||||
|     <a ngbNavLink>{{logFile}}.log</a> | ||||
|   </li> | ||||
|   <div *ngIf="isLoading && !logFiles.length" class="pb-2"> | ||||
|     <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|     <ng-container i18n>Loading...</ng-container> | ||||
|   </div> | ||||
| </ul> | ||||
|  | ||||
| <div [ngbNavOutlet]="nav" class="mt-2"></div> | ||||
|  | ||||
| <div class="bg-dark p-3 text-light font-monospace log-container" #logContainer> | ||||
|   <div *ngIf="isLoading && logFiles.length"> | ||||
|     <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|     <ng-container i18n>Loading...</ng-container> | ||||
|   </div> | ||||
|   <p | ||||
|     class="m-0 p-0 log-entry-{{getLogLevel(log)}}" | ||||
|     *ngFor="let log of logs">{{log}}</p> | ||||
|   | ||||
| @@ -4,7 +4,9 @@ import { | ||||
|   OnInit, | ||||
|   AfterViewChecked, | ||||
|   ViewChild, | ||||
|   OnDestroy, | ||||
| } from '@angular/core' | ||||
| import { Subject, takeUntil } from 'rxjs' | ||||
| import { LogService } from 'src/app/services/rest/log.service' | ||||
|  | ||||
| @Component({ | ||||
| @@ -12,40 +14,60 @@ import { LogService } from 'src/app/services/rest/log.service' | ||||
|   templateUrl: './logs.component.html', | ||||
|   styleUrls: ['./logs.component.scss'], | ||||
| }) | ||||
| export class LogsComponent implements OnInit, AfterViewChecked { | ||||
| export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { | ||||
|   constructor(private logService: LogService) {} | ||||
|  | ||||
|   logs: string[] = [] | ||||
|   public logs: string[] = [] | ||||
|  | ||||
|   logFiles: string[] = [] | ||||
|   public logFiles: string[] = [] | ||||
|  | ||||
|   activeLog: string | ||||
|   public activeLog: string | ||||
|  | ||||
|   private unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   public isLoading: boolean = false | ||||
|  | ||||
|   @ViewChild('logContainer') logContainer: ElementRef | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.logService.list().subscribe((result) => { | ||||
|       this.logFiles = result | ||||
|       if (this.logFiles.length > 0) { | ||||
|         this.activeLog = this.logFiles[0] | ||||
|         this.reloadLogs() | ||||
|       } | ||||
|     }) | ||||
|     this.isLoading = true | ||||
|     this.logService | ||||
|       .list() | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((result) => { | ||||
|         this.logFiles = result | ||||
|         this.isLoading = false | ||||
|         if (this.logFiles.length > 0) { | ||||
|           this.activeLog = this.logFiles[0] | ||||
|           this.reloadLogs() | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   ngAfterViewChecked() { | ||||
|     this.scrollToBottom() | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
|     this.unsubscribeNotifier.next(true) | ||||
|     this.unsubscribeNotifier.complete() | ||||
|   } | ||||
|  | ||||
|   reloadLogs() { | ||||
|     this.logService.get(this.activeLog).subscribe({ | ||||
|       next: (result) => { | ||||
|         this.logs = result | ||||
|       }, | ||||
|       error: () => { | ||||
|         this.logs = [] | ||||
|       }, | ||||
|     }) | ||||
|     this.isLoading = true | ||||
|     this.logService | ||||
|       .get(this.activeLog) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: (result) => { | ||||
|           this.logs = result | ||||
|           this.isLoading = false | ||||
|         }, | ||||
|         error: () => { | ||||
|           this.logs = [] | ||||
|           this.isLoading = false | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   getLogLevel(log: string) { | ||||
|   | ||||
| @@ -24,6 +24,12 @@ | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr *ngIf="isLoading"> | ||||
|       <td colspan="5"> | ||||
|         <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|         <ng-container i18n>Loading...</ng-container> | ||||
|       </td> | ||||
|     </tr> | ||||
|     <tr *ngFor="let object of data"> | ||||
|       <td scope="row">{{ object.name }}</td> | ||||
|       <td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td> | ||||
| @@ -69,7 +75,7 @@ | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <div class="d-flex"> | ||||
| <div class="d-flex" *ngIf="!isLoading"> | ||||
|   <div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div> | ||||
|   <ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination> | ||||
| </div> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import { | ||||
| } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { Subject, Subscription } from 'rxjs' | ||||
| import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | ||||
| import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators' | ||||
| import { | ||||
|   MatchingModel, | ||||
|   MATCHING_ALGORITHMS, | ||||
| @@ -76,8 +76,10 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|   public sortField: string | ||||
|   public sortReverse: boolean | ||||
|  | ||||
|   public isLoading: boolean = false | ||||
|  | ||||
|   private nameFilterDebounce: Subject<string> | ||||
|   private subscription: Subscription | ||||
|   private unsubscribeNotifier: Subject<any> = new Subject() | ||||
|   private _nameFilter: string | ||||
|  | ||||
|   ngOnInit(): void { | ||||
| @@ -85,8 +87,12 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|  | ||||
|     this.nameFilterDebounce = new Subject<string>() | ||||
|  | ||||
|     this.subscription = this.nameFilterDebounce | ||||
|       .pipe(debounceTime(400), distinctUntilChanged()) | ||||
|     this.nameFilterDebounce | ||||
|       .pipe( | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         debounceTime(400), | ||||
|         distinctUntilChanged() | ||||
|       ) | ||||
|       .subscribe((title) => { | ||||
|         this._nameFilter = title | ||||
|         this.page = 1 | ||||
| @@ -95,7 +101,8 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
|     this.subscription.unsubscribe() | ||||
|     this.unsubscribeNotifier.next(true) | ||||
|     this.unsubscribeNotifier.complete() | ||||
|   } | ||||
|  | ||||
|   getMatching(o: MatchingModel) { | ||||
| @@ -119,6 +126,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|   } | ||||
|  | ||||
|   reloadData() { | ||||
|     this.isLoading = true | ||||
|     this.service | ||||
|       .listFiltered( | ||||
|         this.page, | ||||
| @@ -128,9 +136,11 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|         this._nameFilter, | ||||
|         true | ||||
|       ) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((c) => { | ||||
|         this.data = c.results | ||||
|         this.collectionSize = c.count | ||||
|         this.isLoading = false | ||||
|       }) | ||||
|   } | ||||
|  | ||||
| @@ -192,19 +202,22 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|     activeModal.componentInstance.btnCaption = $localize`Delete` | ||||
|     activeModal.componentInstance.confirmClicked.subscribe(() => { | ||||
|       activeModal.componentInstance.buttonsEnabled = false | ||||
|       this.service.delete(object).subscribe({ | ||||
|         next: () => { | ||||
|           activeModal.close() | ||||
|           this.reloadData() | ||||
|         }, | ||||
|         error: (error) => { | ||||
|           activeModal.componentInstance.buttonsEnabled = true | ||||
|           this.toastService.showError( | ||||
|             $localize`Error while deleting element`, | ||||
|             error | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|       this.service | ||||
|         .delete(object) | ||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|         .subscribe({ | ||||
|           next: () => { | ||||
|             activeModal.close() | ||||
|             this.reloadData() | ||||
|           }, | ||||
|           error: (error) => { | ||||
|             activeModal.componentInstance.buttonsEnabled = true | ||||
|             this.toastService.showError( | ||||
|               $localize`Error while deleting element`, | ||||
|               error | ||||
|             ) | ||||
|           }, | ||||
|         }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { Subject, first } from 'rxjs' | ||||
| import { first } from 'rxjs' | ||||
| import { PaperlessTask } from 'src/app/data/paperless-task' | ||||
| import { TasksService } from 'src/app/services/tasks.service' | ||||
| import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||
| @@ -18,7 +18,6 @@ export class TasksComponent | ||||
| { | ||||
|   public activeTab: string | ||||
|   public selectedTasks: Set<number> = new Set() | ||||
|   private unsubscribeNotifer = new Subject() | ||||
|   public expandedTask: number | ||||
|  | ||||
|   public pageSize: number = 25 | ||||
| @@ -43,7 +42,7 @@ export class TasksComponent | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
|     this.unsubscribeNotifer.next(true) | ||||
|     this.tasksService.cancelPending() | ||||
|   } | ||||
|  | ||||
|   dismissTask(task: PaperlessTask) { | ||||
|   | ||||
| @@ -103,6 +103,7 @@ describe('DocumentListViewService', () => { | ||||
|   }) | ||||
|  | ||||
|   afterEach(() => { | ||||
|     documentListViewService.cancelPending() | ||||
|     httpTestingController.verify() | ||||
|     sessionStorage.clear() | ||||
|   }) | ||||
| @@ -425,4 +426,13 @@ describe('DocumentListViewService', () => { | ||||
|     }) | ||||
|     expect(documentListViewService.selected.size).toEqual(3) | ||||
|   }) | ||||
|  | ||||
|   it('should cancel on reload the list', () => { | ||||
|     const cancelSpy = jest.spyOn(documentListViewService, 'cancelPending') | ||||
|     documentListViewService.reload() | ||||
|     httpTestingController.expectOne( | ||||
|       `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9` | ||||
|     ) | ||||
|     expect(cancelSpy).toHaveBeenCalled() | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { Injectable } from '@angular/core' | ||||
| import { ParamMap, Router } from '@angular/router' | ||||
| import { Observable, first } from 'rxjs' | ||||
| import { Observable, Subject, first, takeUntil } from 'rxjs' | ||||
| import { FilterRule } from '../data/filter-rule' | ||||
| import { | ||||
|   filterRulesDiffer, | ||||
| @@ -82,6 +82,8 @@ export class DocumentListViewService { | ||||
|  | ||||
|   currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) | ||||
|  | ||||
|   private unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   private listViewStates: Map<number, ListViewState> = new Map() | ||||
|  | ||||
|   private _activeSavedViewId: number = null | ||||
| @@ -143,6 +145,10 @@ export class DocumentListViewService { | ||||
|     return this.listViewStates.get(this._activeSavedViewId) | ||||
|   } | ||||
|  | ||||
|   public cancelPending(): void { | ||||
|     this.unsubscribeNotifier.next(true) | ||||
|   } | ||||
|  | ||||
|   activateSavedView(view: PaperlessSavedView) { | ||||
|     this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null | ||||
|     if (view) { | ||||
| @@ -210,6 +216,7 @@ export class DocumentListViewService { | ||||
|   } | ||||
|  | ||||
|   reload(onFinish?, updateQueryParams: boolean = true) { | ||||
|     this.cancelPending() | ||||
|     this.isReloading = true | ||||
|     this.error = null | ||||
|     let activeListViewState = this.activeListViewState | ||||
| @@ -222,6 +229,7 @@ export class DocumentListViewService { | ||||
|         activeListViewState.filterRules, | ||||
|         { truncate_content: true } | ||||
|       ) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: (result) => { | ||||
|           this.initialized = true | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Injectable } from '@angular/core' | ||||
| import { first } from 'rxjs/operators' | ||||
| import { Subject } from 'rxjs' | ||||
| import { first, takeUntil } from 'rxjs/operators' | ||||
| import { | ||||
|   PaperlessTask, | ||||
|   PaperlessTaskStatus, | ||||
| @@ -14,10 +15,12 @@ import { environment } from 'src/environments/environment' | ||||
| export class TasksService { | ||||
|   private baseUrl: string = environment.apiBaseUrl | ||||
|  | ||||
|   loading: boolean | ||||
|   public loading: boolean | ||||
|  | ||||
|   private fileTasks: PaperlessTask[] = [] | ||||
|  | ||||
|   private unsubscribeNotifer: Subject<any> = new Subject() | ||||
|  | ||||
|   public get total(): number { | ||||
|     return this.fileTasks.length | ||||
|   } | ||||
| @@ -51,7 +54,7 @@ export class TasksService { | ||||
|  | ||||
|     this.http | ||||
|       .get<PaperlessTask[]>(`${this.baseUrl}tasks/`) | ||||
|       .pipe(first()) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifer), first()) | ||||
|       .subscribe((r) => { | ||||
|         this.fileTasks = r.filter((t) => t.type == PaperlessTaskType.File) // they're all File tasks, for now | ||||
|         this.loading = false | ||||
| @@ -63,9 +66,13 @@ export class TasksService { | ||||
|       .post(`${this.baseUrl}acknowledge_tasks/`, { | ||||
|         tasks: [...task_ids], | ||||
|       }) | ||||
|       .pipe(first()) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifer), first()) | ||||
|       .subscribe((r) => { | ||||
|         this.reload() | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   public cancelPending(): void { | ||||
|     this.unsubscribeNotifer.next(true) | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon