Fixhancement: more log viewer improvements (#11426)

This commit is contained in:
shamoon
2025-11-21 15:52:12 -08:00
committed by GitHub
parent a96db50b0a
commit 93338a0a82
5 changed files with 64 additions and 26 deletions

View File

@@ -145,6 +145,10 @@ HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext typeof HTMLCanvasElement.prototype.getContext
>jest.fn() >jest.fn()
if (!HTMLElement.prototype.scrollTo) {
HTMLElement.prototype.scrollTo = jest.fn()
}
jest.mock('uuid', () => ({ jest.mock('uuid', () => ({
v4: jest.fn(() => v4: jest.fn(() =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => { 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => {

View File

@@ -41,21 +41,21 @@
} }
</ul> </ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div> <div #logContainer class="bg-dark text-light font-monospace log-container p-3" (scroll)="onScroll()">
<cdk-virtual-scroll-viewport
itemSize="20"
class="bg-dark p-3 text-light font-monospace log-container"
#logContainer>
@if (loading && !logFiles.length) { @if (loading && !logFiles.length) {
<div> <div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
</div> </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" </div>
class="m-0 p-0" <button
[ngClass]="'log-entry-' + log.level"> type="button"
{{log.message}} class="btn btn-sm btn-secondary jump-to-bottom position-fixed bottom-0 end-0 m-5"
</p> [class.visible]="showJumpToBottom"
</cdk-virtual-scroll-viewport> (click)="scrollToBottom()"
>
<span i18n>Jump to bottom</span>
</button>

View File

@@ -16,11 +16,21 @@
} }
.log-container { .log-container {
overflow-y: scroll; height: calc(100vh - 190px);
height: calc(100vh - 200px); overflow-y: auto;
top: 0;
p { p {
white-space: pre-wrap; 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;
}

View File

@@ -110,4 +110,11 @@ describe('LogsComponent', () => {
jest.advanceTimersByTime(1) jest.advanceTimersByTime(1)
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 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)
})
}) })

View File

@@ -1,11 +1,8 @@
import {
CdkVirtualScrollViewport,
ScrollingModule,
} from '@angular/cdk/scrolling'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild, ViewChild,
@@ -28,8 +25,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
CommonModule, CommonModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
CdkVirtualScrollViewport,
ScrollingModule,
], ],
}) })
export class LogsComponent export class LogsComponent
@@ -49,9 +44,11 @@ export class LogsComponent
public limit: number = 5000 public limit: number = 5000
public showJumpToBottom = false
private readonly limitChange$ = new Subject<number>() private readonly limitChange$ = new Subject<number>()
@ViewChild('logContainer') logContainer: CdkVirtualScrollViewport @ViewChild('logContainer') logContainer: ElementRef<HTMLElement>
ngOnInit(): void { ngOnInit(): void {
this.limitChange$ this.limitChange$
@@ -89,6 +86,7 @@ export class LogsComponent
reloadLogs() { reloadLogs() {
this.loading = true this.loading = true
const shouldStickToBottom = this.isNearBottom()
this.logService this.logService
.get(this.activeLog, this.limit) .get(this.activeLog, this.limit)
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
@@ -108,7 +106,10 @@ export class LogsComponent
}) })
if (hasChanges) { if (hasChanges) {
this.logs = parsed this.logs = parsed
this.scrollToBottom() if (shouldStickToBottom) {
this.scrollToBottom()
}
this.showJumpToBottom = !shouldStickToBottom
} }
}, },
error: () => { error: () => {
@@ -142,9 +143,25 @@ export class LogsComponent
} }
scrollToBottom(): void { scrollToBottom(): void {
this.changedetectorRef.detectChanges() const viewport = this.logContainer?.nativeElement
if (this.logContainer) { if (!viewport) {
this.logContainer.scrollToIndex(this.logs.length - 1) 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()
} }
} }