]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Frontend better handle slow backend requests 4055/head
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 23 Aug 2023 05:11:53 +0000 (22:11 -0700)
committershamoon <4887959+shamoon@users.noreply.github.com>
Sat, 9 Sep 2023 02:49:54 +0000 (19:49 -0700)
13 files changed:
src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html
src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts
src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html
src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts
src-ui/src/app/components/document-list/document-list.component.ts
src-ui/src/app/components/manage/logs/logs.component.html
src-ui/src/app/components/manage/logs/logs.component.ts
src-ui/src/app/components/manage/management-list/management-list.component.html
src-ui/src/app/components/manage/management-list/management-list.component.ts
src-ui/src/app/components/manage/tasks/tasks.component.ts
src-ui/src/app/services/document-list-view.service.spec.ts
src-ui/src/app/services/document-list-view.service.ts
src-ui/src/app/services/tasks.service.ts

index ad790821e40bff945cfbb915fc4dd23b26fd9514..4f9410bee2bb9f7ecb915df4b64961f8f79c5d9f 100644 (file)
@@ -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>
index b1493a7d3beb0f6611f09936b252471dc50cc149..d8d482033e5690ce0dedd1589fdde83e2781c627 100644 (file)
@@ -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 ||
index 49218799aba1f9f5451ea0d7a240838e14a99039..8cac4dce87abc72b0d02dfab487301d7eddba72f 100644 (file)
@@ -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>
index 5264acda8d69bad3624db77737f8850eb0177fd4..1e13702b69665638756c8847371dc5a734722179 100644 (file)
@@ -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
index 25a95401f65bb823bb6a7fd4ab4b8d41f648ffa5..911ddccbf34f23d381835cb7e8273ca718611859 100644 (file)
@@ -185,7 +185,7 @@ export class DocumentListComponent
   }
 
   ngOnDestroy() {
-    // unsubscribes all
+    this.list.cancelPending()
     this.unsubscribeNotifier.next(this)
     this.unsubscribeNotifier.complete()
   }
index 2129a8fa1cffe6567d7201c2c646caf695f200ac..f16a6e5295600eeb4df296ed615c4963a43cea1e 100644 (file)
@@ -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>
index 2da52e9504a723e904fe536ab443c482627c2f03..e2fc2ab0bcb6e38d77f7d80db6f0cafdcd2a2b11 100644 (file)
@@ -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) {
index 377bafbead81b642f3bcb353e95d7c88b519116a..b0fba8962f84d554a37178c2b5c6cec65302f006 100644 (file)
     </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>
index cfe82ec451dea55394e9b2a923af3cb8a6682bec..c4e2ef0ea795cb9bf23f4245566947bfc41b7995 100644 (file)
@@ -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
+            )
+          },
+        })
     })
   }
 
index b4dcbcc21f42c9bc444380b7965e89871274506c..01b5669cd9f88ad2c0e897285cc69cb88989bd95 100644 (file)
@@ -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) {
index 2a63e21c90e97e3b0c5009175e73086dcb32135f..5fcfd532725b04b07568a90012134edffecc102c 100644 (file)
@@ -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()
+  })
 })
index ab8556c332694b7c13267404c96a56f7700130ac..4fb2cda0232e29568d89ae40b8e020daa264d352 100644 (file)
@@ -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
index 9dfd118e76a3579c60d6761b5df799388888d876..662dd1015d2b4af71856e8bd57d6834360b7b15d 100644 (file)
@@ -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)
+  }
 }