diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index cf79d162f..977539289 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -297,11 +297,11 @@ src/app/components/app-frame/app-frame.component.html - 82 + 84 src/app/components/app-frame/app-frame.component.html - 84 + 86 src/app/components/dashboard/dashboard.component.html @@ -316,11 +316,11 @@ src/app/components/app-frame/app-frame.component.html - 89 + 91 src/app/components/app-frame/app-frame.component.html - 91 + 93 src/app/components/document-list/document-list.component.ts @@ -363,11 +363,11 @@ src/app/components/app-frame/app-frame.component.html - 253 + 255 src/app/components/app-frame/app-frame.component.html - 255 + 257 @@ -658,11 +658,11 @@ src/app/components/app-frame/app-frame.component.html - 288 + 290 src/app/components/app-frame/app-frame.component.html - 291 + 293 @@ -672,11 +672,33 @@ 4 + + Show + + src/app/components/admin/logs/logs.component.html + 8 + + + src/app/components/document-list/document-list.component.html + 37 + + + src/app/components/manage/saved-views/saved-views.component.html + 52 + + + + lines + + src/app/components/admin/logs/logs.component.html + 17 + + Auto refresh src/app/components/admin/logs/logs.component.html - 8 + 21 src/app/components/admin/tasks/tasks.component.html @@ -687,11 +709,11 @@ Loading... src/app/components/admin/logs/logs.component.html - 24 + 38 src/app/components/admin/logs/logs.component.html - 36 + 53 src/app/components/admin/tasks/tasks.component.html @@ -1003,11 +1025,11 @@ src/app/components/app-frame/app-frame.component.html - 213 + 215 src/app/components/app-frame/app-frame.component.html - 215 + 217 src/app/components/manage/saved-views/saved-views.component.html @@ -1572,7 +1594,7 @@ src/app/components/app-frame/app-frame.component.ts - 167 + 180 @@ -1583,11 +1605,11 @@ src/app/components/app-frame/app-frame.component.html - 276 + 278 src/app/components/app-frame/app-frame.component.html - 278 + 280 @@ -1999,11 +2021,11 @@ src/app/components/app-frame/app-frame.component.html - 236 + 238 src/app/components/app-frame/app-frame.component.html - 239 + 241 @@ -2368,11 +2390,11 @@ src/app/components/app-frame/app-frame.component.html - 267 + 269 src/app/components/app-frame/app-frame.component.html - 269 + 271 @@ -2709,58 +2731,58 @@ src/app/components/app-frame/app-frame.component.html - 297 + 299 src/app/components/app-frame/app-frame.component.html - 300 + 302 Saved views src/app/components/app-frame/app-frame.component.html - 99 + 101 src/app/components/app-frame/app-frame.component.html - 104 + 106 Open documents src/app/components/app-frame/app-frame.component.html - 139 + 141 Close all src/app/components/app-frame/app-frame.component.html - 159 + 161 src/app/components/app-frame/app-frame.component.html - 161 + 163 Manage src/app/components/app-frame/app-frame.component.html - 170 + 172 Correspondents src/app/components/app-frame/app-frame.component.html - 176 + 178 src/app/components/app-frame/app-frame.component.html - 178 + 180 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2771,11 +2793,11 @@ Tags src/app/components/app-frame/app-frame.component.html - 183 + 185 src/app/components/app-frame/app-frame.component.html - 186 + 188 src/app/components/common/input/tags/tags.component.ts @@ -2806,11 +2828,11 @@ Document Types src/app/components/app-frame/app-frame.component.html - 192 + 194 src/app/components/app-frame/app-frame.component.html - 194 + 196 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2821,11 +2843,11 @@ Storage Paths src/app/components/app-frame/app-frame.component.html - 199 + 201 src/app/components/app-frame/app-frame.component.html - 201 + 203 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2836,11 +2858,11 @@ Custom Fields src/app/components/app-frame/app-frame.component.html - 206 + 208 src/app/components/app-frame/app-frame.component.html - 208 + 210 src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html @@ -2855,11 +2877,11 @@ Workflows src/app/components/app-frame/app-frame.component.html - 222 + 224 src/app/components/app-frame/app-frame.component.html - 224 + 226 src/app/components/manage/workflows/workflows.component.html @@ -2870,92 +2892,92 @@ Mail src/app/components/app-frame/app-frame.component.html - 229 + 231 src/app/components/app-frame/app-frame.component.html - 232 + 234 Administration src/app/components/app-frame/app-frame.component.html - 247 + 249 Configuration src/app/components/app-frame/app-frame.component.html - 260 + 262 src/app/components/app-frame/app-frame.component.html - 262 + 264 GitHub src/app/components/app-frame/app-frame.component.html - 307 + 309 is available. src/app/components/app-frame/app-frame.component.html - 316,317 + 318,319 Click to view. src/app/components/app-frame/app-frame.component.html - 317 + 319 Paperless-ngx can automatically check for updates src/app/components/app-frame/app-frame.component.html - 321 + 323 How does this work? src/app/components/app-frame/app-frame.component.html - 328,330 + 330,332 Update available src/app/components/app-frame/app-frame.component.html - 341 + 343 Sidebar views updated src/app/components/app-frame/app-frame.component.ts - 251 + 264 Error updating sidebar views src/app/components/app-frame/app-frame.component.ts - 254 + 267 An error occurred while saving update checking settings. src/app/components/app-frame/app-frame.component.ts - 275 + 288 @@ -7259,25 +7281,25 @@ Print failed. src/app/components/document-detail/document-detail.component.ts - 1455 + 1460 Error loading document for printing. src/app/components/document-detail/document-detail.component.ts - 1463 + 1472 An error occurred loading tiff: src/app/components/document-detail/document-detail.component.ts - 1528 + 1537 src/app/components/document-detail/document-detail.component.ts - 1532 + 1541 @@ -7881,17 +7903,6 @@ 45 - - Show - - src/app/components/document-list/document-list.component.html - 37 - - - src/app/components/manage/saved-views/saved-views.component.html - 52 - - Sort diff --git a/src-ui/src/app/components/admin/logs/logs.component.html b/src-ui/src/app/components/admin/logs/logs.component.html index d6685d857..bdc80583f 100644 --- a/src-ui/src/app/components/admin/logs/logs.component.html +++ b/src-ui/src/app/components/admin/logs/logs.component.html @@ -3,9 +3,23 @@ i18n-title info="Review the log files for the application and for email checking." i18n-info> -
- - +
+
+ Show + + lines +
+
+ + +
@@ -29,14 +43,19 @@
-
+ @if (loading && logFiles.length) {
Loading...
} - @for (log of logs; track $index) { -

{{log}}

- } -
+

+ {{log.message}} +

+ diff --git a/src-ui/src/app/components/admin/logs/logs.component.scss b/src-ui/src/app/components/admin/logs/logs.component.scss index 834c8c1cb..56fd2e8f3 100644 --- a/src-ui/src/app/components/admin/logs/logs.component.scss +++ b/src-ui/src/app/components/admin/logs/logs.component.scss @@ -18,7 +18,7 @@ .log-container { overflow-y: scroll; height: calc(100vh - 200px); - top: 70px; + top: 0; p { white-space: pre-wrap; diff --git a/src-ui/src/app/components/admin/logs/logs.component.spec.ts b/src-ui/src/app/components/admin/logs/logs.component.spec.ts index 6e4adacfe..728916830 100644 --- a/src-ui/src/app/components/admin/logs/logs.component.spec.ts +++ b/src-ui/src/app/components/admin/logs/logs.component.spec.ts @@ -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,13 +62,12 @@ 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() }) it('should display logs with first log initially', () => { - expect(logSpy).toHaveBeenCalledWith('paperless') + expect(logSpy).toHaveBeenCalledWith('paperless', 5000) fixture.detectChanges() expect(fixture.debugElement.nativeElement.textContent).toContain( paperless_logs[0] @@ -71,7 +78,7 @@ describe('LogsComponent', () => { fixture.debugElement .queryAll(By.directive(NgbNavLink))[1] .nativeElement.dispatchEvent(new MouseEvent('click')) - expect(logSpy).toHaveBeenCalledWith('mail') + expect(logSpy).toHaveBeenCalledWith('mail', 5000) }) it('should handle error with no logs', () => { @@ -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) @@ -90,4 +101,13 @@ describe('LogsComponent', () => { jest.advanceTimersByTime(6000) expect(reloadSpy).toHaveBeenCalledTimes(2) }) + + it('should debounce limit changes before reloading logs', () => { + const initialCalls = reloadSpy.mock.calls.length + component.onLimitChange(6000) + jest.advanceTimersByTime(299) + expect(reloadSpy).toHaveBeenCalledTimes(initialCalls) + jest.advanceTimersByTime(1) + expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1) + }) }) diff --git a/src-ui/src/app/components/admin/logs/logs.component.ts b/src-ui/src/app/components/admin/logs/logs.component.ts index 4799b6125..68b88265d 100644 --- a/src-ui/src/app/components/admin/logs/logs.component.ts +++ b/src-ui/src/app/components/admin/logs/logs.component.ts @@ -1,7 +1,11 @@ +import { + CdkVirtualScrollViewport, + ScrollingModule, +} from '@angular/cdk/scrolling' +import { CommonModule } from '@angular/common' import { ChangeDetectorRef, Component, - ElementRef, OnDestroy, OnInit, ViewChild, @@ -9,7 +13,7 @@ import { } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' -import { filter, takeUntil, timer } from 'rxjs' +import { Subject, debounceTime, filter, takeUntil, timer } from 'rxjs' import { LogService } from 'src/app/services/rest/log.service' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' @@ -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,9 +47,17 @@ export class LogsComponent public autoRefreshEnabled: boolean = true - @ViewChild('logContainer') logContainer: ElementRef + public limit: number = 5000 + + private readonly limitChange$ = new Subject() + + @ViewChild('logContainer') logContainer: CdkVirtualScrollViewport ngOnInit(): void { + this.limitChange$ + .pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier)) + .subscribe(() => this.reloadLogs()) + this.logService .list() .pipe(takeUntil(this.unsubscribeNotifier)) @@ -68,16 +83,33 @@ export class LogsComponent super.ngOnDestroy() } + onLimitChange(limit: number): void { + this.limitChange$.next(limit) + } + reloadLogs() { this.loading = true this.logService - .get(this.activeLog) + .get(this.activeLog, this.limit) .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe({ next: (result) => { - this.logs = result this.loading = false - this.scrollToBottom() + const parsed = this.parseLogsWithLevel(result) + const hasChanges = + parsed.length !== this.logs.length || + parsed.some((log, idx) => { + const current = this.logs[idx] + return ( + !current || + current.message !== log.message || + current.level !== log.level + ) + }) + if (hasChanges) { + this.logs = parsed + this.scrollToBottom() + } }, error: () => { this.logs = [] @@ -100,12 +132,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) + } } } diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 7ec92cda8..673eaf03b 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -68,13 +68,15 @@