Add debugging console
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
import {
|
||||
Component,
|
||||
HostListener,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { lucideBug } from '@ng-icons/lucide';
|
||||
|
||||
import { DebuggingService, type DebugLogLevel } from '../../../core/services/debugging.service';
|
||||
import { DebugConsoleEntryListComponent } from './debug-console-entry-list/debug-console-entry-list.component';
|
||||
import { DebugConsoleNetworkMapComponent } from './debug-console-network-map/debug-console-network-map.component';
|
||||
import { DebugConsoleToolbarComponent } from './debug-console-toolbar/debug-console-toolbar.component';
|
||||
|
||||
type DebugLevelState = Record<DebugLogLevel, boolean>;
|
||||
|
||||
type DebugConsoleTab = 'logs' | 'network';
|
||||
type DebugConsoleLauncherVariant = 'floating' | 'inline' | 'compact';
|
||||
|
||||
@Component({
|
||||
selector: 'app-debug-console',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
DebugConsoleEntryListComponent,
|
||||
DebugConsoleNetworkMapComponent,
|
||||
DebugConsoleToolbarComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideBug
|
||||
})
|
||||
],
|
||||
templateUrl: './debug-console.component.html',
|
||||
host: {
|
||||
style: 'display: contents;',
|
||||
'data-debug-console-root': 'true'
|
||||
}
|
||||
})
|
||||
export class DebugConsoleComponent {
|
||||
readonly debugging = inject(DebuggingService);
|
||||
readonly entries = this.debugging.entries;
|
||||
readonly isOpen = this.debugging.isConsoleOpen;
|
||||
readonly networkSnapshot = this.debugging.networkSnapshot;
|
||||
readonly launcherVariant = input<DebugConsoleLauncherVariant>('floating');
|
||||
readonly showLauncher = input(true);
|
||||
readonly showPanel = input(true);
|
||||
|
||||
readonly activeTab = signal<DebugConsoleTab>('logs');
|
||||
readonly detached = signal(false);
|
||||
readonly searchTerm = signal('');
|
||||
readonly selectedSource = signal('all');
|
||||
readonly autoScroll = signal(true);
|
||||
readonly panelHeight = signal(360);
|
||||
readonly panelWidth = signal(832);
|
||||
readonly panelLeft = signal(0);
|
||||
readonly panelTop = signal(0);
|
||||
readonly levelState = signal<DebugLevelState>({
|
||||
event: true,
|
||||
info: true,
|
||||
warn: true,
|
||||
error: true,
|
||||
debug: true
|
||||
});
|
||||
|
||||
readonly sourceOptions = computed(() => {
|
||||
return Array.from(new Set(this.entries().map((entry) => entry.source))).sort();
|
||||
});
|
||||
readonly filteredEntries = computed(() => {
|
||||
const searchTerm = this.searchTerm().trim()
|
||||
.toLowerCase();
|
||||
const selectedSource = this.selectedSource();
|
||||
const levelState = this.levelState();
|
||||
|
||||
return this.entries().filter((entry) => {
|
||||
if (!levelState[entry.level])
|
||||
return false;
|
||||
|
||||
if (selectedSource !== 'all' && entry.source !== selectedSource)
|
||||
return false;
|
||||
|
||||
if (!searchTerm)
|
||||
return true;
|
||||
|
||||
return [
|
||||
entry.message,
|
||||
entry.source,
|
||||
entry.level,
|
||||
entry.timeLabel,
|
||||
entry.dateTimeLabel,
|
||||
entry.payloadText || ''
|
||||
].some((value) => value.toLowerCase().includes(searchTerm));
|
||||
});
|
||||
});
|
||||
readonly levelCounts = computed<Record<DebugLogLevel, number>>(() => {
|
||||
const counts: Record<DebugLogLevel, number> = {
|
||||
event: 0,
|
||||
info: 0,
|
||||
warn: 0,
|
||||
error: 0,
|
||||
debug: 0
|
||||
};
|
||||
|
||||
for (const entry of this.entries()) {
|
||||
counts[entry.level] += entry.count;
|
||||
}
|
||||
|
||||
return counts;
|
||||
});
|
||||
readonly visibleCount = computed(() => {
|
||||
return this.filteredEntries().reduce((sum, entry) => sum + entry.count, 0);
|
||||
});
|
||||
readonly badgeCount = computed(() => {
|
||||
const counts = this.levelCounts();
|
||||
|
||||
return counts.error > 0 ? counts.error : this.entries().reduce((sum, entry) => sum + entry.count, 0);
|
||||
});
|
||||
readonly hasErrors = computed(() => this.levelCounts().error > 0);
|
||||
readonly networkSummary = computed(() => this.networkSnapshot().summary);
|
||||
|
||||
private dragging = false;
|
||||
private resizingHeight = false;
|
||||
private resizingWidth = false;
|
||||
private resizeOriginY = 0;
|
||||
private resizeOriginX = 0;
|
||||
private resizeOriginHeight = 360;
|
||||
private resizeOriginWidth = 832;
|
||||
private panelOriginLeft = 0;
|
||||
private panelOriginTop = 0;
|
||||
|
||||
constructor() {
|
||||
this.syncPanelBounds();
|
||||
|
||||
effect(() => {
|
||||
const selectedSource = this.selectedSource();
|
||||
const sourceOptions = this.sourceOptions();
|
||||
|
||||
if (selectedSource !== 'all' && !sourceOptions.includes(selectedSource))
|
||||
this.selectedSource.set('all');
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:mousemove', ['$event'])
|
||||
onResizeMove(event: MouseEvent): void {
|
||||
if (this.dragging) {
|
||||
this.updateDetachedPosition(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resizingWidth) {
|
||||
this.updatePanelWidth(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.resizingHeight)
|
||||
return;
|
||||
|
||||
this.updatePanelHeight(event);
|
||||
}
|
||||
|
||||
@HostListener('window:mouseup')
|
||||
onResizeEnd(): void {
|
||||
this.dragging = false;
|
||||
this.resizingHeight = false;
|
||||
this.resizingWidth = false;
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onWindowResize(): void {
|
||||
this.syncPanelBounds();
|
||||
}
|
||||
|
||||
toggleConsole(): void {
|
||||
this.debugging.toggleConsole();
|
||||
}
|
||||
|
||||
closeConsole(): void {
|
||||
this.debugging.closeConsole();
|
||||
}
|
||||
|
||||
updateSearchTerm(value: string): void {
|
||||
this.searchTerm.set(value);
|
||||
}
|
||||
|
||||
updateSelectedSource(source: string): void {
|
||||
this.selectedSource.set(source);
|
||||
}
|
||||
|
||||
setActiveTab(tab: DebugConsoleTab): void {
|
||||
this.activeTab.set(tab);
|
||||
}
|
||||
|
||||
toggleDetached(): void {
|
||||
const nextDetached = !this.detached();
|
||||
|
||||
this.detached.set(nextDetached);
|
||||
this.syncPanelBounds();
|
||||
|
||||
if (nextDetached)
|
||||
this.initializeDetachedPosition();
|
||||
}
|
||||
|
||||
toggleLevel(level: DebugLogLevel): void {
|
||||
this.levelState.update((current) => ({
|
||||
...current,
|
||||
[level]: !current[level]
|
||||
}));
|
||||
}
|
||||
|
||||
toggleAutoScroll(): void {
|
||||
this.autoScroll.update((enabled) => !enabled);
|
||||
}
|
||||
|
||||
clearLogs(): void {
|
||||
this.debugging.clear();
|
||||
}
|
||||
|
||||
startResize(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.resizingHeight = true;
|
||||
this.resizeOriginY = event.clientY;
|
||||
this.resizeOriginHeight = this.panelHeight();
|
||||
this.panelOriginTop = this.panelTop();
|
||||
}
|
||||
|
||||
startWidthResize(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.resizingWidth = true;
|
||||
this.resizeOriginX = event.clientX;
|
||||
this.resizeOriginWidth = this.panelWidth();
|
||||
this.panelOriginLeft = this.panelLeft();
|
||||
}
|
||||
|
||||
startDrag(event: MouseEvent): void {
|
||||
if (!this.detached())
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.dragging = true;
|
||||
this.resizeOriginX = event.clientX;
|
||||
this.resizeOriginY = event.clientY;
|
||||
this.panelOriginLeft = this.panelLeft();
|
||||
this.panelOriginTop = this.panelTop();
|
||||
}
|
||||
|
||||
formatBadgeCount(count: number): string {
|
||||
if (count > 99)
|
||||
return '99+';
|
||||
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
private updatePanelHeight(event: MouseEvent): void {
|
||||
const delta = this.resizeOriginY - event.clientY;
|
||||
const nextHeight = this.clampPanelHeight(this.resizeOriginHeight + delta);
|
||||
|
||||
this.panelHeight.set(nextHeight);
|
||||
|
||||
if (!this.detached())
|
||||
return;
|
||||
|
||||
const originBottom = this.panelOriginTop + this.resizeOriginHeight;
|
||||
const maxTop = this.getMaxPanelTop(nextHeight);
|
||||
|
||||
this.panelTop.set(this.clampValue(originBottom - nextHeight, 16, maxTop));
|
||||
}
|
||||
|
||||
private updatePanelWidth(event: MouseEvent): void {
|
||||
const delta = this.resizeOriginX - event.clientX;
|
||||
const nextWidth = this.clampPanelWidth(this.resizeOriginWidth + delta);
|
||||
|
||||
this.panelWidth.set(nextWidth);
|
||||
|
||||
if (!this.detached())
|
||||
return;
|
||||
|
||||
const originRight = this.panelOriginLeft + this.resizeOriginWidth;
|
||||
const maxLeft = this.getMaxPanelLeft(nextWidth);
|
||||
|
||||
this.panelLeft.set(this.clampValue(originRight - nextWidth, 16, maxLeft));
|
||||
}
|
||||
|
||||
private updateDetachedPosition(event: MouseEvent): void {
|
||||
const nextLeft = this.panelOriginLeft + (event.clientX - this.resizeOriginX);
|
||||
const nextTop = this.panelOriginTop + (event.clientY - this.resizeOriginY);
|
||||
|
||||
this.panelLeft.set(this.clampValue(nextLeft, 16, this.getMaxPanelLeft(this.panelWidth())));
|
||||
this.panelTop.set(this.clampValue(nextTop, 16, this.getMaxPanelTop(this.panelHeight())));
|
||||
}
|
||||
|
||||
private initializeDetachedPosition(): void {
|
||||
if (this.panelLeft() > 0 || this.panelTop() > 0) {
|
||||
this.clampDetachedPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
this.panelLeft.set(this.getMaxPanelLeft(this.panelWidth()));
|
||||
this.panelTop.set(this.clampValue(window.innerHeight - this.panelHeight() - 96, 16, this.getMaxPanelTop(this.panelHeight())));
|
||||
}
|
||||
|
||||
private clampPanelHeight(height: number): number {
|
||||
const maxHeight = this.detached()
|
||||
? Math.max(260, window.innerHeight - 32)
|
||||
: Math.floor(window.innerHeight * 0.75);
|
||||
|
||||
return Math.min(Math.max(height, 260), maxHeight);
|
||||
}
|
||||
|
||||
private clampPanelWidth(width: number): number {
|
||||
const maxWidth = Math.max(360, window.innerWidth - 32);
|
||||
const minWidth = Math.min(460, maxWidth);
|
||||
|
||||
return Math.min(Math.max(width, minWidth), maxWidth);
|
||||
}
|
||||
|
||||
private clampDetachedPosition(): void {
|
||||
this.panelLeft.set(this.clampValue(this.panelLeft(), 16, this.getMaxPanelLeft(this.panelWidth())));
|
||||
this.panelTop.set(this.clampValue(this.panelTop(), 16, this.getMaxPanelTop(this.panelHeight())));
|
||||
}
|
||||
|
||||
private getMaxPanelLeft(width: number): number {
|
||||
return Math.max(16, window.innerWidth - width - 16);
|
||||
}
|
||||
|
||||
private getMaxPanelTop(height: number): number {
|
||||
return Math.max(16, window.innerHeight - height - 16);
|
||||
}
|
||||
|
||||
private syncPanelBounds(): void {
|
||||
this.panelWidth.update((width) => this.clampPanelWidth(width));
|
||||
this.panelHeight.update((height) => this.clampPanelHeight(height));
|
||||
|
||||
if (this.detached())
|
||||
this.clampDetachedPosition();
|
||||
}
|
||||
|
||||
private clampValue(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user