]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Fixhancement: more log viewer improvements (#11426)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Fri, 21 Nov 2025 23:52:12 +0000 (15:52 -0800)
committerGitHub <noreply@github.com>
Fri, 21 Nov 2025 23:52:12 +0000 (15:52 -0800)
src-ui/setup-jest.ts
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 c7bcabddb5a3fe1dd0dbbcc930d7f5ae3c0efd00..df5e9d175355d910bfd9d56a49c72c6d26e4edf2 100644 (file)
@@ -145,6 +145,10 @@ HTMLCanvasElement.prototype.getContext = <
   typeof HTMLCanvasElement.prototype.getContext
 >jest.fn()
 
+if (!HTMLElement.prototype.scrollTo) {
+  HTMLElement.prototype.scrollTo = jest.fn()
+}
+
 jest.mock('uuid', () => ({
   v4: jest.fn(() =>
     'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => {
index 70dcda86d321564b8870120a6d4f19ccf23c4bd3..b3df5ff89690aa103b0581c9840a50e52f844d88 100644 (file)
   }
 </ul>
 
-<div [ngbNavOutlet]="nav" class="mt-2"></div>
-
-<cdk-virtual-scroll-viewport
-  itemSize="20"
-  class="bg-dark p-3 text-light font-monospace log-container"
-  #logContainer>
+<div #logContainer class="bg-dark text-light font-monospace log-container p-3" (scroll)="onScroll()">
   @if (loading && !logFiles.length) {
     <div>
       <div class="spinner-border spinner-border-sm me-2" role="status"></div>
       <ng-container i18n>Loading...</ng-container>
     </div>
+  } @else {
+    <p *ngFor="let log of logs" class="m-0 p-0" [ngClass]="'log-entry-' + log.level">{{log.message}}</p>
   }
-  <p *cdkVirtualFor="let log of logs"
-     class="m-0 p-0"
-     [ngClass]="'log-entry-' + log.level">
-    {{log.message}}
-  </p>
-</cdk-virtual-scroll-viewport>
+</div>
+<button
+  type="button"
+  class="btn btn-sm btn-secondary jump-to-bottom position-fixed bottom-0 end-0 m-5"
+  [class.visible]="showJumpToBottom"
+  (click)="scrollToBottom()"
+>
+  ↓ <span i18n>Jump to bottom</span>
+</button>
index 56fd2e8f36fa64f049db6b5720fb75d288194f1e..25ab0b8ac48697dd58e5f49daccf6569fc81af1d 100644 (file)
 }
 
 .log-container {
-  overflow-y: scroll;
-  height: calc(100vh - 200px);
-  top: 0;
+  height: calc(100vh - 190px);
+  overflow-y: auto;
 
   p {
     white-space: pre-wrap;
   }
 }
+
+.jump-to-bottom {
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 120ms ease-in-out;
+}
+
+.jump-to-bottom.visible {
+  opacity: 1;
+  pointer-events: auto;
+}
index 728916830eaeeefa1369e533827583766c54b777..1b84254279e3ef49f1a37d2ffa2891914f877173 100644 (file)
@@ -110,4 +110,11 @@ describe('LogsComponent', () => {
     jest.advanceTimersByTime(1)
     expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
   })
+
+  it('should update jump to bottom visibility on scroll', () => {
+    component.showJumpToBottom = false
+    jest.spyOn(component as any, 'isNearBottom').mockReturnValue(false)
+    component.onScroll()
+    expect(component.showJumpToBottom).toBe(true)
+  })
 })
index 68b88265da9a7e66d9cd9474a9902b6e631be8a2..e186b27b09c4d6cc3dfa3a80866e6246880390bf 100644 (file)
@@ -1,11 +1,8 @@
-import {
-  CdkVirtualScrollViewport,
-  ScrollingModule,
-} from '@angular/cdk/scrolling'
 import { CommonModule } from '@angular/common'
 import {
   ChangeDetectorRef,
   Component,
+  ElementRef,
   OnDestroy,
   OnInit,
   ViewChild,
@@ -28,8 +25,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
     CommonModule,
     FormsModule,
     ReactiveFormsModule,
-    CdkVirtualScrollViewport,
-    ScrollingModule,
   ],
 })
 export class LogsComponent
@@ -49,9 +44,11 @@ export class LogsComponent
 
   public limit: number = 5000
 
+  public showJumpToBottom = false
+
   private readonly limitChange$ = new Subject<number>()
 
-  @ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
+  @ViewChild('logContainer') logContainer: ElementRef<HTMLElement>
 
   ngOnInit(): void {
     this.limitChange$
@@ -89,6 +86,7 @@ export class LogsComponent
 
   reloadLogs() {
     this.loading = true
+    const shouldStickToBottom = this.isNearBottom()
     this.logService
       .get(this.activeLog, this.limit)
       .pipe(takeUntil(this.unsubscribeNotifier))
@@ -108,7 +106,10 @@ export class LogsComponent
             })
           if (hasChanges) {
             this.logs = parsed
-            this.scrollToBottom()
+            if (shouldStickToBottom) {
+              this.scrollToBottom()
+            }
+            this.showJumpToBottom = !shouldStickToBottom
           }
         },
         error: () => {
@@ -142,9 +143,25 @@ export class LogsComponent
   }
 
   scrollToBottom(): void {
-    this.changedetectorRef.detectChanges()
-    if (this.logContainer) {
-      this.logContainer.scrollToIndex(this.logs.length - 1)
+    const viewport = this.logContainer?.nativeElement
+    if (!viewport) {
+      return
     }
+    this.changedetectorRef.detectChanges()
+    viewport.scrollTop = viewport.scrollHeight
+    this.showJumpToBottom = false
+  }
+
+  private isNearBottom(): boolean {
+    if (!this.logContainer?.nativeElement) return true
+    const distanceFromBottom =
+      this.logContainer.nativeElement.scrollHeight -
+      this.logContainer.nativeElement.scrollTop -
+      this.logContainer.nativeElement.clientHeight
+    return distanceFromBottom <= 40
+  }
+
+  onScroll(): void {
+    this.showJumpToBottom = !this.isNearBottom()
   }
 }