<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>
.log-container {
overflow-y: scroll;
height: calc(100vh - 200px);
- top: 70px;
+ top: 0;
p {
white-space: pre-wrap;
+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'
NgxBootstrapIconsModule.pick(allIcons),
LogsComponent,
PageHeaderComponent,
+ CommonModule,
+ CdkVirtualScrollViewport,
+ ScrollingModule,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
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 auto refresh, allow toggle', () => {
+ jest
+ .spyOn(CdkVirtualScrollViewport.prototype, 'scrollToIndex')
+ .mockImplementation(() => undefined)
+
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
+import {
+ CdkVirtualScrollViewport,
+ ScrollingModule,
+} from '@angular/cdk/scrolling'
+import { CommonModule } from '@angular/common'
import {
ChangeDetectorRef,
Component,
- ElementRef,
OnDestroy,
OnInit,
ViewChild,
imports: [
PageHeaderComponent,
NgbNavModule,
+ CommonModule,
FormsModule,
ReactiveFormsModule,
+ CdkVirtualScrollViewport,
+ ScrollingModule,
],
})
export class LogsComponent
private logService = inject(LogService)
private changedetectorRef = inject(ChangeDetectorRef)
- public logs: string[] = []
+ public logs: Array<{ message: string; level: number }> = []
public logFiles: string[] = []
public autoRefreshEnabled: boolean = true
- @ViewChild('logContainer') logContainer: ElementRef
+ @ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
ngOnInit(): void {
this.logService
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
- this.logs = result
+ this.logs = this.parseLogsWithLevel(result)
this.loading = false
this.scrollToBottom()
},
}
}
+ 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)
+ }
}
}