]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Performance: use virtual scroll container and log level parsing for logs view (#11233)
authorCanbiZ <47820557+MickLesk@users.noreply.github.com>
Thu, 30 Oct 2025 23:34:53 +0000 (16:34 -0700)
committerGitHub <noreply@github.com>
Thu, 30 Oct 2025 23:34:53 +0000 (23:34 +0000)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
src-ui/src/app/components/admin/logs/logs.component.html
src-ui/src/app/components/admin/logs/logs.component.scss
src-ui/src/app/components/admin/logs/logs.component.spec.ts
src-ui/src/app/components/admin/logs/logs.component.ts

index d6685d857af960bffbd13c969e7e58041a7604e9..21df9f33bdf134a4c21f8809830f8bc8f6df32c5 100644 (file)
 
 <div [ngbNavOutlet]="nav" class="mt-2"></div>
 
-<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
+<cdk-virtual-scroll-viewport
+  itemSize="20"
+  class="bg-dark p-3 text-light font-monospace log-container"
+  #logContainer>
   @if (loading && logFiles.length) {
     <div>
       <div class="spinner-border spinner-border-sm me-2" role="status"></div>
       <ng-container i18n>Loading...</ng-container>
     </div>
   }
-  @for (log of logs; track $index) {
-    <p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
-  }
-</div>
+  <p *cdkVirtualFor="let log of logs"
+     class="m-0 p-0"
+     [ngClass]="'log-entry-' + log.level">
+    {{log.message}}
+  </p>
+</cdk-virtual-scroll-viewport>
index 834c8c1cb953247dc957de183c6aecd00be0e074..56fd2e8f36fa64f049db6b5720fb75d288194f1e 100644 (file)
@@ -18,7 +18,7 @@
 .log-container {
   overflow-y: scroll;
   height: calc(100vh - 200px);
-  top: 70px;
+  top: 0;
 
   p {
     white-space: pre-wrap;
index 6e4adacfe04e78086904b6a01d95fb4ccb859c69..841fec44d57ecf6e35e726d87640dc6c039da5db 100644 (file)
@@ -1,3 +1,8 @@
+import {
+  CdkVirtualScrollViewport,
+  ScrollingModule,
+} from '@angular/cdk/scrolling'
+import { CommonModule } from '@angular/common'
 import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 import { provideHttpClientTesting } from '@angular/common/http/testing'
 import { ComponentFixture, TestBed } from '@angular/core/testing'
@@ -38,6 +43,9 @@ describe('LogsComponent', () => {
         NgxBootstrapIconsModule.pick(allIcons),
         LogsComponent,
         PageHeaderComponent,
+        CommonModule,
+        CdkVirtualScrollViewport,
+        ScrollingModule,
       ],
       providers: [
         provideHttpClient(withInterceptorsFromDi()),
@@ -54,7 +62,6 @@ describe('LogsComponent', () => {
     fixture = TestBed.createComponent(LogsComponent)
     component = fixture.componentInstance
     reloadSpy = jest.spyOn(component, 'reloadLogs')
-    window.HTMLElement.prototype.scroll = function () {} // mock scroll
     jest.useFakeTimers()
     fixture.detectChanges()
   })
@@ -83,6 +90,10 @@ describe('LogsComponent', () => {
   })
 
   it('should auto refresh, allow toggle', () => {
+    jest
+      .spyOn(CdkVirtualScrollViewport.prototype, 'scrollToIndex')
+      .mockImplementation(() => undefined)
+
     jest.advanceTimersByTime(6000)
     expect(reloadSpy).toHaveBeenCalledTimes(2)
 
index 4799b612527f9706277b513c5381f9629fd774e2..488f9db264923292eebb029c3d2d6d9ca767d690 100644 (file)
@@ -1,7 +1,11 @@
+import {
+  CdkVirtualScrollViewport,
+  ScrollingModule,
+} from '@angular/cdk/scrolling'
+import { CommonModule } from '@angular/common'
 import {
   ChangeDetectorRef,
   Component,
-  ElementRef,
   OnDestroy,
   OnInit,
   ViewChild,
@@ -21,8 +25,11 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
   imports: [
     PageHeaderComponent,
     NgbNavModule,
+    CommonModule,
     FormsModule,
     ReactiveFormsModule,
+    CdkVirtualScrollViewport,
+    ScrollingModule,
   ],
 })
 export class LogsComponent
@@ -32,7 +39,7 @@ export class LogsComponent
   private logService = inject(LogService)
   private changedetectorRef = inject(ChangeDetectorRef)
 
-  public logs: string[] = []
+  public logs: Array<{ message: string; level: number }> = []
 
   public logFiles: string[] = []
 
@@ -40,7 +47,7 @@ export class LogsComponent
 
   public autoRefreshEnabled: boolean = true
 
-  @ViewChild('logContainer') logContainer: ElementRef
+  @ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
 
   ngOnInit(): void {
     this.logService
@@ -75,7 +82,7 @@ export class LogsComponent
       .pipe(takeUntil(this.unsubscribeNotifier))
       .subscribe({
         next: (result) => {
-          this.logs = result
+          this.logs = this.parseLogsWithLevel(result)
           this.loading = false
           this.scrollToBottom()
         },
@@ -100,12 +107,19 @@ export class LogsComponent
     }
   }
 
+  private parseLogsWithLevel(
+    logs: string[]
+  ): Array<{ message: string; level: number }> {
+    return logs.map((log) => ({
+      message: log,
+      level: this.getLogLevel(log),
+    }))
+  }
+
   scrollToBottom(): void {
     this.changedetectorRef.detectChanges()
-    this.logContainer?.nativeElement.scroll({
-      top: this.logContainer.nativeElement.scrollHeight,
-      left: 0,
-      behavior: 'auto',
-    })
+    if (this.logContainer) {
+      this.logContainer.scrollToIndex(this.logs.length - 1)
+    }
   }
 }